From 2d957cd46f4a1d05c1af4e8af23a22270fd05e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Korina=20=C5=A0imi=C4=8Devi=C4=87?= Date: Thu, 5 Oct 2023 13:59:32 +0200 Subject: [PATCH] [mqtt-client] boost-like project folder structure Summary: resolves T12767 Reviewers: ivica Reviewed By: ivica Subscribers: miljen Tags: #spacetime Maniphest Tasks: T12767 Differential Revision: https://repo.mireo.local/D25970 --- SConscript | 78 +++ example/openssl-tls.cpp | 282 +++++++++ example/src/run_examples.cpp | 15 + example/tcp.cpp | 152 +++++ example/websocket-tcp.cpp | 173 ++++++ example/websocket-tls.cpp | 250 ++++++++ include/async_mqtt5.hpp | 9 + include/async_mqtt5/detail/async_mutex.hpp | 210 +++++++ include/async_mqtt5/detail/async_traits.hpp | 161 +++++ .../detail/cancellable_handler.hpp | 151 +++++ include/async_mqtt5/detail/control_packet.hpp | 153 +++++ include/async_mqtt5/detail/internal_types.hpp | 62 ++ include/async_mqtt5/detail/ring_buffer.hpp | 416 +++++++++++++ include/async_mqtt5/detail/spinlock.hpp | 58 ++ include/async_mqtt5/error.hpp | 423 +++++++++++++ include/async_mqtt5/impl/assemble_op.hpp | 231 +++++++ include/async_mqtt5/impl/async_sender.hpp | 235 +++++++ .../async_mqtt5/impl/autoconnect_stream.hpp | 196 ++++++ include/async_mqtt5/impl/client_service.hpp | 278 +++++++++ include/async_mqtt5/impl/connect_op.hpp | 291 +++++++++ include/async_mqtt5/impl/disconnect_op.hpp | 140 +++++ include/async_mqtt5/impl/endpoints.hpp | 215 +++++++ .../async_mqtt5/impl/internal/alloc/memory.h | 42 ++ .../impl/internal/alloc/memory_resource.h | 515 ++++++++++++++++ .../async_mqtt5/impl/internal/alloc/string.h | 22 + .../async_mqtt5/impl/internal/alloc/vector.h | 15 + .../impl/internal/codecs/base_decoders.hpp | 408 +++++++++++++ .../impl/internal/codecs/base_encoders.hpp | 492 +++++++++++++++ .../impl/internal/codecs/message_decoders.hpp | 284 +++++++++ .../impl/internal/codecs/message_encoders.hpp | 422 +++++++++++++ .../impl/internal/codecs/traits.hpp | 33 + include/async_mqtt5/impl/ping_op.hpp | 97 +++ include/async_mqtt5/impl/publish_rec_op.hpp | 204 +++++++ include/async_mqtt5/impl/publish_send_op.hpp | 383 ++++++++++++ include/async_mqtt5/impl/read_message_op.hpp | 127 ++++ include/async_mqtt5/impl/read_op.hpp | 117 ++++ include/async_mqtt5/impl/reconnect_op.hpp | 190 ++++++ include/async_mqtt5/impl/replies.hpp | 186 ++++++ include/async_mqtt5/impl/sentry_op.hpp | 91 +++ include/async_mqtt5/impl/subscribe_op.hpp | 171 ++++++ include/async_mqtt5/impl/unsubscribe_op.hpp | 173 ++++++ include/async_mqtt5/impl/write_op.hpp | 100 +++ include/async_mqtt5/mqtt_client.hpp | 230 +++++++ include/async_mqtt5/property_types.hpp | 167 +++++ include/async_mqtt5/types.hpp | 272 +++++++++ test/experimental/cancellation.cpp | 155 +++++ test/experimental/memory.cpp | 50 ++ test/experimental/message_assembling.cpp | 199 ++++++ test/experimental/mutex.cpp | 237 +++++++ test/experimental/uri_parse.cpp | 46 ++ test/unit/include/test_common/delayed_op.hpp | 111 ++++ .../include/test_common/message_exchange.hpp | 254 ++++++++ test/unit/include/test_common/packet_util.hpp | 126 ++++ .../include/test_common/protocol_logging.hpp | 21 + test/unit/include/test_common/test_broker.hpp | 324 ++++++++++ .../unit/include/test_common/test_service.hpp | 49 ++ test/unit/include/test_common/test_stream.hpp | 337 ++++++++++ test/unit/src/run_tests.cpp | 21 + test/unit/test/client_broker.cpp | 576 ++++++++++++++++++ test/unit/test/coroutine.cpp | 139 +++++ test/unit/test/publish_send_op.cpp | 139 +++++ test/unit/test/serialization.cpp | 298 +++++++++ win/mqtt-client.sln | 41 ++ win/mqtt-client.vcxproj | 189 ++++++ win/mqtt-client.vcxproj.filters | 174 ++++++ win/test/mqtt-test.vcxproj | 158 +++++ win/test/mqtt-test.vcxproj.filters | 63 ++ 67 files changed, 12627 insertions(+) create mode 100644 SConscript create mode 100644 example/openssl-tls.cpp create mode 100644 example/src/run_examples.cpp create mode 100644 example/tcp.cpp create mode 100644 example/websocket-tcp.cpp create mode 100644 example/websocket-tls.cpp create mode 100644 include/async_mqtt5.hpp create mode 100644 include/async_mqtt5/detail/async_mutex.hpp create mode 100644 include/async_mqtt5/detail/async_traits.hpp create mode 100644 include/async_mqtt5/detail/cancellable_handler.hpp create mode 100644 include/async_mqtt5/detail/control_packet.hpp create mode 100644 include/async_mqtt5/detail/internal_types.hpp create mode 100644 include/async_mqtt5/detail/ring_buffer.hpp create mode 100644 include/async_mqtt5/detail/spinlock.hpp create mode 100644 include/async_mqtt5/error.hpp create mode 100644 include/async_mqtt5/impl/assemble_op.hpp create mode 100644 include/async_mqtt5/impl/async_sender.hpp create mode 100644 include/async_mqtt5/impl/autoconnect_stream.hpp create mode 100644 include/async_mqtt5/impl/client_service.hpp create mode 100644 include/async_mqtt5/impl/connect_op.hpp create mode 100644 include/async_mqtt5/impl/disconnect_op.hpp create mode 100644 include/async_mqtt5/impl/endpoints.hpp create mode 100644 include/async_mqtt5/impl/internal/alloc/memory.h create mode 100644 include/async_mqtt5/impl/internal/alloc/memory_resource.h create mode 100644 include/async_mqtt5/impl/internal/alloc/string.h create mode 100644 include/async_mqtt5/impl/internal/alloc/vector.h create mode 100644 include/async_mqtt5/impl/internal/codecs/base_decoders.hpp create mode 100644 include/async_mqtt5/impl/internal/codecs/base_encoders.hpp create mode 100644 include/async_mqtt5/impl/internal/codecs/message_decoders.hpp create mode 100644 include/async_mqtt5/impl/internal/codecs/message_encoders.hpp create mode 100644 include/async_mqtt5/impl/internal/codecs/traits.hpp create mode 100644 include/async_mqtt5/impl/ping_op.hpp create mode 100644 include/async_mqtt5/impl/publish_rec_op.hpp create mode 100644 include/async_mqtt5/impl/publish_send_op.hpp create mode 100644 include/async_mqtt5/impl/read_message_op.hpp create mode 100644 include/async_mqtt5/impl/read_op.hpp create mode 100644 include/async_mqtt5/impl/reconnect_op.hpp create mode 100644 include/async_mqtt5/impl/replies.hpp create mode 100644 include/async_mqtt5/impl/sentry_op.hpp create mode 100644 include/async_mqtt5/impl/subscribe_op.hpp create mode 100644 include/async_mqtt5/impl/unsubscribe_op.hpp create mode 100644 include/async_mqtt5/impl/write_op.hpp create mode 100644 include/async_mqtt5/mqtt_client.hpp create mode 100644 include/async_mqtt5/property_types.hpp create mode 100644 include/async_mqtt5/types.hpp create mode 100644 test/experimental/cancellation.cpp create mode 100644 test/experimental/memory.cpp create mode 100644 test/experimental/message_assembling.cpp create mode 100644 test/experimental/mutex.cpp create mode 100644 test/experimental/uri_parse.cpp create mode 100644 test/unit/include/test_common/delayed_op.hpp create mode 100644 test/unit/include/test_common/message_exchange.hpp create mode 100644 test/unit/include/test_common/packet_util.hpp create mode 100644 test/unit/include/test_common/protocol_logging.hpp create mode 100644 test/unit/include/test_common/test_broker.hpp create mode 100644 test/unit/include/test_common/test_service.hpp create mode 100644 test/unit/include/test_common/test_stream.hpp create mode 100644 test/unit/src/run_tests.cpp create mode 100644 test/unit/test/client_broker.cpp create mode 100644 test/unit/test/coroutine.cpp create mode 100644 test/unit/test/publish_send_op.cpp create mode 100644 test/unit/test/serialization.cpp create mode 100644 win/mqtt-client.sln create mode 100644 win/mqtt-client.vcxproj create mode 100644 win/mqtt-client.vcxproj.filters create mode 100644 win/test/mqtt-test.vcxproj create mode 100644 win/test/mqtt-test.vcxproj.filters diff --git a/SConscript b/SConscript new file mode 100644 index 0000000..a99589e --- /dev/null +++ b/SConscript @@ -0,0 +1,78 @@ +import glob + +Import('ctx') + +ctx.Project('#/3rdParty/openssl') + +sources = [ + 'example/tcp.cpp', + # commented out to speed up compiling + # 'example/openssl-tls.cpp', + # 'example/websocket-tcp.cpp', + # 'example/websocket-tls.cpp', + 'example/src/run_examples.cpp', +] + +test_sources = [ + # 'test/experimental/cancellation.cpp', + # 'test/experimental/message_assembling.cpp', + # 'test/experimental/memory.cpp', + # 'test/experimental/mutex.cpp', + # 'test/experimental/uri_parse.cpp', + 'test/unit/test/serialization.cpp', + 'test/unit/test/publish_send_op.cpp', + 'test/unit/test/client_broker.cpp', + 'test/unit/test/coroutine.cpp', + 'test/unit/src/run_tests.cpp' +] + +includes = [ + 'include', + '#/3rdParty/openssl/include' +] + +test_includes = [ + 'include', + 'test/unit/include', + '#/3rdParty/openssl/include' +] + +libs = { + 'all': Split('openssl'), +} + +defines = { + 'all' : ['BOOST_ALL_NO_LIB', 'BOOST_NO_TYPEID', '_REENTRANT'], + 'toolchain:llvm' : ['BOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF'], +} + +test_defines = { + 'all' : ['BOOST_ALL_NO_LIB', 'BOOST_NO_TYPEID', 'BOOST_TEST_NO_MAIN=1','_REENTRANT'], + 'toolchain:llvm' : ['BOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF'], +} + +# add ' -ftemplate-backtrace-limit=1' to cxxflags when necessary +cxxflags = { + 'all': Split('-fexceptions -frtti -Wall -Wno-unused-local-typedefs -ftemplate-backtrace-limit=1'), +} + +frameworks = { + 'os:macos': Split('Security'), +} + +ctx.Program(name='mqtt-examples', + source=sources, + includes=includes, + defines=defines, + CXXFLAGs=cxxflags, + libraries=libs, + frameworks=frameworks, +) + +ctx.Program(name='mqtt-tests', + source=test_sources, + includes=test_includes, + defines=test_defines, + CXXFLAGs=cxxflags, + frameworks=frameworks, +) diff --git a/example/openssl-tls.cpp b/example/openssl-tls.cpp new file mode 100644 index 0000000..309ee44 --- /dev/null +++ b/example/openssl-tls.cpp @@ -0,0 +1,282 @@ +#include + +#include +#include +#include +#include +#include + +#include + +namespace asio = boost::asio; + +namespace async_mqtt5 { + +template +struct tls_handshake_type> { + static constexpr auto client = asio::ssl::stream_base::client; + static constexpr auto server = asio::ssl::stream_base::server; +}; + +template +void assign_tls_sni( + const authority_path& ap, + asio::ssl::context& ctx, + asio::ssl::stream& stream +) { + SSL_set_tlsext_host_name(stream.native_handle(), ap.host.c_str()); +} + +} // end namespace async_mqtt5 + +constexpr char spacetime_ca[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIDYDCCAkigAwIBAgIUZZsEKT8m+uGZRNMaTuCiZBchSU4wDQYJKoZIhvcNAQEL\n" + "BQAwHTEbMBkGA1UEAwwSTWlyZW8gU3BhY2VUaW1lIENBMB4XDTIzMDIwNzIwMzU1\n" + "MFoXDTMzMDIwNDIwMzU1MFowHTEbMBkGA1UEAwwSTWlyZW8gU3BhY2VUaW1lIENB\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzZshi2nJNyYZ4aJN+q27\n" + "wA69lUAwRSHiJGBCGzppLue/LFDDC1t8GDicjYLGH5eJOlFwr8TbAr+ZH+/PyBoS\n" + "7g5tsSn5xZhgEaivnq1MJNqYWHqW5KF2KhGxzzyC6m3JFK21H0xiJu9ej2wQs1tD\n" + "ZWG3Y7pKeMFhCezEip5ueIyvmjsenK00TJKr6w1Rkr4BA40euLb5r0srWllKKUyl\n" + "t5AEFghdVU7GeXfC2LPrzzMVngFWTaoL3QRf7VMhvNC0Xq7h2yjwd4wROYiJFZBj\n" + "UgDSi2W50fPlVDliET2hPBR6lQPgCBRoIdQF8NneSBJ5xH+mw9ZZV8btL8ahwWtL\n" + "GwIDAQABo4GXMIGUMB0GA1UdDgQWBBSM9pLZlAekgqt7ZXzPOdTEifMLmzBYBgNV\n" + "HSMEUTBPgBSM9pLZlAekgqt7ZXzPOdTEifMLm6EhpB8wHTEbMBkGA1UEAwwSTWly\n" + "ZW8gU3BhY2VUaW1lIENBghRlmwQpPyb64ZlE0xpO4KJkFyFJTjAMBgNVHRMEBTAD\n" + "AQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAuSe6ZOwc8KnNXs1M\n" + "KoShOUxZGDFBUJFNAtTSsMi0ap6GIo/yJr+6SAkHkVU0HFkl5lzRo9aUHRw4O7Ez\n" + "579JMzUDdEGBxtYqda0Rxnw8N2mq5Fxpv+1b6v4GsWA30k6TdqnrFdNpFVI84W6u\n" + "Fw3HTKA0Ah0jXryc1kC1jU7mYKf66TDI5PSbuZRjHgQzzyUXZmCn1WcLbvunsc4r\n" + "Tk2FrfXHfvag12yPLc9aIOrtfRW2wtlZcxMzX4oE6wfllAIIsSZGx0muydiMe8bw\n" + "Od5S0p1sspsWOthj1t9yhHMwznwV81QLePWzgGmml21uA067ZGG8NHxNbERd/9e+\n" + "Qz9m6w==\n" + "-----END CERTIFICATE-----\n" +; + +void publish_qos0_openssl_tls() { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = asio::ssl::stream; + asio::ssl::context tls_context(asio::ssl::context::tls_client); + + error_code ec; + tls_context.add_certificate_authority(asio::buffer(spacetime_ca), ec); + tls_context.set_verify_mode(asio::ssl::verify_peer); + + using client_type = mqtt_client; + client_type c(ioc, "", std::move(tls_context)); + + c.credentials("test-qos0-openssl-tls", "", "") + .brokers("iot.fcluster.mireo.hr/mqtt", 8883) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos0!", + retain_e::no, publish_props{}, + [&c](error_code ec) { + fmt::print("[Test-publish-qos0-openssl-tls] error_code: {}\n", ec.message()); + c.cancel(); + } + ); + + ioc.run(); + return; +} + +void publish_qos1_openssl_tls() { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = asio::ssl::stream; + asio::ssl::context tls_context(asio::ssl::context::tls_client); + + error_code ec; + tls_context.add_certificate_authority(asio::buffer(spacetime_ca), ec); + tls_context.set_verify_mode(asio::ssl::verify_peer); + + using client_type = mqtt_client; + client_type c(ioc, "", std::move(tls_context)); + + c.credentials("test-qos1-openssl-tls", "", "") + .brokers("iot.fcluster.mireo.hr/mqtt", 8883) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos1!", + retain_e::no, publish_props{}, + [&c](error_code ec, reason_code rc, puback_props) { + fmt::print( + "[Test-publish-qos1-openssl-tls] " + "error_code: {}, reason_code: {}\n", ec.message(), rc.message() + ); + c.cancel(); + } + ); + + ioc.run(); + return; +} + + +void publish_qos2_openssl_tls() { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = asio::ssl::stream; + asio::ssl::context tls_context(asio::ssl::context::tls_client); + + error_code ec; + tls_context.add_certificate_authority(asio::buffer(spacetime_ca), ec); + tls_context.set_verify_mode(asio::ssl::verify_peer); + + using client_type = mqtt_client; + client_type c(ioc, "", std::move(tls_context)); + + c.credentials("test-qos2-openssl-tls", "", "") + .brokers("iot.fcluster.mireo.hr/mqtt", 8883) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos2!", + retain_e::no, publish_props{}, + [&c](error_code ec, reason_code rc, pubcomp_props) { + fmt::print( + "[Test-publish-qos2-openssl-tls] " + "error_code: {}, reason_code: {}\n", ec.message(), rc.message() + ); + c.cancel(); + } + ); + + ioc.run(); + return; +} + + +void subscribe_and_receive_openssl_tls(int num_receive) { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = asio::ssl::stream; + asio::ssl::context tls_context(asio::ssl::context::tls_client); + + error_code ec; + tls_context.add_certificate_authority(asio::buffer(spacetime_ca), ec); + tls_context.set_verify_mode(asio::ssl::verify_peer); + + using client_type = mqtt_client; + client_type c(ioc, "", std::move(tls_context)); + + c.credentials("test-subscriber-openssl-tls", "", "") + .brokers("iot.fcluster.mireo.hr/mqtt", 8883) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + + std::vector topics; + topics.push_back(subscribe_topic { + "test/mqtt-test", { + qos_e::exactly_once, + subscribe_options::no_local_e::no, + subscribe_options::retain_as_published_e::retain, + subscribe_options::retain_handling_e::send + } + }); + + c.async_subscribe( + topics, subscribe_props {}, + [](error_code ec, std::vector codes, suback_props) { + if (ec == asio::error::operation_aborted) + return; + fmt::print( + "[Test-subscribe-and-receive-openssl-tls] subscribe error_code: {}," + " reason_code: {}\n", ec.message(), codes[0].message() + ); + } + ); + + + for (auto i = 0; i < num_receive; i++) { + c.async_receive( + [&c, i, num_receive] ( + error_code ec, std::string topic, + std::string payload, publish_props + ) { + if (ec == asio::error::operation_aborted) + return; + fmt::print( + "[Test-subscribe-and-receive-openssl-tls] message {}/{}:" + "ec: {}, topic: {}, payload: {}\n", + i + 1, num_receive, ec.message(), topic, payload + ); + + if (i == num_receive - 1) + c.cancel(); + } + ); + } + + ioc.run(); + return; +} + +void test_coro() { + using namespace async_mqtt5; + asio::io_context ioc; + + co_spawn(ioc, [&ioc]() -> asio::awaitable { + using stream_type = asio::ssl::stream; + asio::ssl::context tls_context(asio::ssl::context::tls_client); + + error_code ec; + tls_context.add_certificate_authority(asio::buffer(spacetime_ca), ec); + tls_context.set_verify_mode(asio::ssl::verify_peer); + + using client_type = mqtt_client; + client_type c(ioc, "", std::move(tls_context)); + + c.credentials("coro-client", "", "") + .brokers("iot.fcluster.mireo.hr/mqtt", 8883) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + std::vector topics; + topics.push_back(subscribe_topic { + "test/mqtt-test", { + qos_e::exactly_once, + subscribe_options::no_local_e::no, + subscribe_options::retain_as_published_e::retain, + subscribe_options::retain_handling_e::send + } + }); + + auto [codes, props] = co_await c.async_subscribe( + topics, subscribe_props {}, asio::use_awaitable + ); + fmt::print("Subscribe result: ({}),", codes[0].message()); + + auto [topic, payload, rec_props] = co_await c.async_receive(asio::use_awaitable); + fmt::print("Receive from topic {}: {}\n", topic, payload); + + asio::steady_timer timer(ioc); + timer.expires_from_now(std::chrono::seconds(1)); + co_await timer.async_wait(asio::use_awaitable); + c.cancel(); + + co_return; + }, asio::detached); + + ioc.run(); +} + +void run_openssl_tls_examples() { + publish_qos0_openssl_tls(); + publish_qos1_openssl_tls(); + publish_qos2_openssl_tls(); + subscribe_and_receive_openssl_tls(1); + test_coro(); +} diff --git a/example/src/run_examples.cpp b/example/src/run_examples.cpp new file mode 100644 index 0000000..d38230f --- /dev/null +++ b/example/src/run_examples.cpp @@ -0,0 +1,15 @@ + +void run_openssl_tls_examples(); +void run_tcp_examples(); +void run_websocket_tcp_examples(); +void run_websocket_tls_examples(); + +int main(int argc, char* argv[]) { + + run_tcp_examples(); + //run_openssl_tls_examples(); + //run_websocket_tcp_examples(); + //run_websocket_tls_examples(); + + return 0; +} diff --git a/example/tcp.cpp b/example/tcp.cpp new file mode 100644 index 0000000..41ed11f --- /dev/null +++ b/example/tcp.cpp @@ -0,0 +1,152 @@ +#include +#include + +#include +#include + +#include + +namespace asio = boost::asio; + +void publish_qos0_tcp() { + fmt::print("[Test-publish-qos0-tcp]\n"); + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = asio::ip::tcp::socket; + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("test-qos0-tcp", "", "") + .brokers("mqtt.mireo.local", 1883) + .will({ "test/mqtt-test", "i died",qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos0!", + retain_e::no, publish_props {}, + [&c](error_code ec) { + fmt::print("\terror_code: {}\n", ec.message()); + c.cancel(); + } + ); + + ioc.run(); +} + + +void publish_qos1_tcp() { + fmt::print("[Test-publish-qos1-tcp]\n"); + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = asio::ip::tcp::socket; + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("test-qos1-tcp", "", "") + .brokers("mqtt.mireo.local", 1883) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos1!", + retain_e::no, publish_props {}, + [&c](error_code ec, reason_code rc, puback_props) { + fmt::print( + "\terror_code: {}, reason_code: {}\n", + ec.message(), rc.message() + ); + c.cancel(); + } + ); + + ioc.run(); +} + +void publish_qos2_tcp() { + fmt::print("[Test-publish-qos2-tcp]\n"); + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = asio::ip::tcp::socket; + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("test-qos2-tcp", "", "") + .brokers("mqtt.mireo.local", 1883) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos2!", + retain_e::no, publish_props{}, + [&c](error_code ec, reason_code rc, pubcomp_props) { + fmt::print( + "\terror_code: {}, reason_code: {}\n", + ec.message(), rc.message() + ); + c.cancel(); + } + ); + + ioc.run(); +} + + +void subscribe_and_receive_tcp(int num_receive) { + fmt::print("[Test-subscribe-and-receive-tcp]\n"); + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = asio::ip::tcp::socket; + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("test-subscriber-tcp", "", "") + .brokers("mqtt.mireo.local", 1883) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_subscribe( + { "test/mqtt-test", { qos_e::exactly_once } }, subscribe_props {}, + [](error_code ec, std::vector codes, suback_props) { + if (ec == asio::error::operation_aborted) + return; + fmt::print( + "\tsubscribe error_code: {}, reason_code: {}\n", + ec.message(), codes[0].message() + ); + } + ); + + + for (auto i = 0; i < num_receive; i++) { + c.async_receive( + [&c, i, num_receive] ( + error_code ec, std::string topic, + std::string payload, publish_props + ) { + if (ec == asio::error::operation_aborted) + return; + fmt::print( + "\tmessage {}/{}: ec: {}, topic: {}, payload: {}\n", + i + 1, num_receive, ec.message(), topic, payload + ); + + if (i == num_receive - 1) + c.cancel(); + } + ); + } + + ioc.run(); +} + + +void run_tcp_examples() { + publish_qos0_tcp(); + publish_qos1_tcp(); + publish_qos2_tcp(); + subscribe_and_receive_tcp(1); +} diff --git a/example/websocket-tcp.cpp b/example/websocket-tcp.cpp new file mode 100644 index 0000000..011c52d --- /dev/null +++ b/example/websocket-tcp.cpp @@ -0,0 +1,173 @@ +#include + +#include +#include +#include + +#include + +namespace asio = boost::asio; + +void publish_qos0_websocket_tcp() { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = boost::beast::websocket::stream< + asio::ip::tcp::socket + >; + + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("test-qos0-websocket-tcp", "", "") + .brokers("fcluster-5/mqtt", 8083) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos0!", + retain_e::no, publish_props{}, + [&c](error_code ec) { + fmt::print("[Test-publish-qos0-websocket-tcp] error_code: {}\n", ec.message()); + c.cancel(); + } + ); + + ioc.run(); + return; +} + +void publish_qos1_websocket_tcp() { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = boost::beast::websocket::stream< + asio::ip::tcp::socket + >; + + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("test-qos1-websocket-tcp", "", "") + .brokers("fcluster-5/mqtt", 8083) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos1!", + async_mqtt5::retain_e::no, publish_props{}, + [&c](error_code ec, reason_code rc, puback_props) { + fmt::print( + "[Test-publish-qos1-websocket-tcp] " + "error_code: {}, reason_code: {}\n", ec.message(), rc.message() + ); + c.cancel(); + } + ); + + ioc.run(); + return; +} + +void publish_qos2_websocket_tcp() { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = boost::beast::websocket::stream< + asio::ip::tcp::socket + >; + + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("test-qos2-websocket-tcp", "", "") + .brokers("fcluster-5/mqtt", 8083) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos2!", + retain_e::no, publish_props{}, + [&c](error_code ec, reason_code rc, pubcomp_props) { + fmt::print( + "[Test-publish-qos2-websocket-tcp] " + "error_code: {}, reason_code: {}\n", ec.message(), rc.message() + ); + c.cancel(); + } + ); + + ioc.run(); + return; +} + + +void subscribe_and_receive_websocket_tcp(int num_receive) { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = boost::beast::websocket::stream< + asio::ip::tcp::socket + >; + + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("test-subscriber-websocket-tcp", "", "") + .brokers("fcluster-5/mqtt", 8083) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + std::vector topics; + topics.push_back(subscribe_topic{ + "test/mqtt-test", { + qos_e::exactly_once, + subscribe_options::no_local_e::no, + subscribe_options::retain_as_published_e::retain, + subscribe_options::retain_handling_e::send + } + }); + + c.async_subscribe( + topics, subscribe_props{}, + [](error_code ec, std::vector codes, suback_props) { + if (ec == asio::error::operation_aborted) + return; + fmt::print( + "[Test-subscribe-and-receive-websocket-tcp] " + " error_code: {}, reason_code: {}\n", ec.message(), codes[0].message() + ); + } + ); + + for (auto i = 0; i < num_receive; i++) { + c.async_receive( + [&c, i, num_receive] ( + error_code ec, std::string topic, + std::string payload, publish_props + ) { + if (ec == asio::error::operation_aborted) + return; + fmt::print( + "[Test-subscribe-and-receive-websocket-tcp] message {}/{}:" + "ec: {}, topic: {}, payload: {}\n", + i + 1, num_receive, ec.message(), topic, payload + ); + + if (i == num_receive - 1) + c.cancel(); + } + ); + } + + ioc.run(); + return; +} + + +void run_websocket_tcp_examples() { + publish_qos0_websocket_tcp(); + publish_qos1_websocket_tcp(); + publish_qos2_websocket_tcp(); + subscribe_and_receive_websocket_tcp(1); +} diff --git a/example/websocket-tls.cpp b/example/websocket-tls.cpp new file mode 100644 index 0000000..09b7150 --- /dev/null +++ b/example/websocket-tls.cpp @@ -0,0 +1,250 @@ +#include + +#include +#include +#include +#include + +#include + +namespace asio = boost::asio; + +namespace boost::beast::websocket { + +template +void async_teardown( + boost::beast::role_type role, + asio::ssl::stream& stream, + TeardownHandler&& handler +) { + return stream.async_shutdown(std::forward(handler)); +} + +} // end namespace boost::beast::websocket + +namespace async_mqtt5 { + +template +struct tls_handshake_type> { + static constexpr auto client = asio::ssl::stream_base::client; + static constexpr auto server = asio::ssl::stream_base::server; +}; + +template +void assign_tls_sni( + const authority_path& ap, + asio::ssl::context& ctx, + asio::ssl::stream& stream +) { + SSL_set_tlsext_host_name(stream.native_handle(), ap.host.c_str()); +} + +} // end namespace async_mqtt5 + +constexpr const char spacetime_ca[] = + "-----BEGIN CERTIFICATE-----\n" + "MIIDYDCCAkigAwIBAgIUZZsEKT8m+uGZRNMaTuCiZBchSU4wDQYJKoZIhvcNAQEL\n" + "BQAwHTEbMBkGA1UEAwwSTWlyZW8gU3BhY2VUaW1lIENBMB4XDTIzMDIwNzIwMzU1\n" + "MFoXDTMzMDIwNDIwMzU1MFowHTEbMBkGA1UEAwwSTWlyZW8gU3BhY2VUaW1lIENB\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzZshi2nJNyYZ4aJN+q27\n" + "wA69lUAwRSHiJGBCGzppLue/LFDDC1t8GDicjYLGH5eJOlFwr8TbAr+ZH+/PyBoS\n" + "7g5tsSn5xZhgEaivnq1MJNqYWHqW5KF2KhGxzzyC6m3JFK21H0xiJu9ej2wQs1tD\n" + "ZWG3Y7pKeMFhCezEip5ueIyvmjsenK00TJKr6w1Rkr4BA40euLb5r0srWllKKUyl\n" + "t5AEFghdVU7GeXfC2LPrzzMVngFWTaoL3QRf7VMhvNC0Xq7h2yjwd4wROYiJFZBj\n" + "UgDSi2W50fPlVDliET2hPBR6lQPgCBRoIdQF8NneSBJ5xH+mw9ZZV8btL8ahwWtL\n" + "GwIDAQABo4GXMIGUMB0GA1UdDgQWBBSM9pLZlAekgqt7ZXzPOdTEifMLmzBYBgNV\n" + "HSMEUTBPgBSM9pLZlAekgqt7ZXzPOdTEifMLm6EhpB8wHTEbMBkGA1UEAwwSTWly\n" + "ZW8gU3BhY2VUaW1lIENBghRlmwQpPyb64ZlE0xpO4KJkFyFJTjAMBgNVHRMEBTAD\n" + "AQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAuSe6ZOwc8KnNXs1M\n" + "KoShOUxZGDFBUJFNAtTSsMi0ap6GIo/yJr+6SAkHkVU0HFkl5lzRo9aUHRw4O7Ez\n" + "579JMzUDdEGBxtYqda0Rxnw8N2mq5Fxpv+1b6v4GsWA30k6TdqnrFdNpFVI84W6u\n" + "Fw3HTKA0Ah0jXryc1kC1jU7mYKf66TDI5PSbuZRjHgQzzyUXZmCn1WcLbvunsc4r\n" + "Tk2FrfXHfvag12yPLc9aIOrtfRW2wtlZcxMzX4oE6wfllAIIsSZGx0muydiMe8bw\n" + "Od5S0p1sspsWOthj1t9yhHMwznwV81QLePWzgGmml21uA067ZGG8NHxNbERd/9e+\n" + "Qz9m6w==\n" + "-----END CERTIFICATE-----\n" +; + +void publish_qos0_websocket_tls() { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = boost::beast::websocket::stream< + asio::ssl::stream + >; + + error_code ec; + asio::ssl::context tls_context(asio::ssl::context::tls_client); + tls_context.add_certificate_authority(asio::buffer(spacetime_ca), ec); + tls_context.set_verify_mode(asio::ssl::verify_peer); + + using client_type = mqtt_client; + client_type c(ioc, "", std::move(tls_context)); + + c.credentials("test-qos0-websocket-tls", "", "") + .brokers("iot.fcluster.mireo.hr/mqtt", 8884) + .will({ "test/mqtt-test", "i died", async_mqtt5::qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos0!", + retain_e::no, publish_props{}, + [&c](error_code ec) { + fmt::print("[Test-publish-qos0-websocket-tls] error_code: {}\n", ec.message()); + c.cancel(); + } + ); + + ioc.run(); + return; +} + +void publish_qos1_websocket_tls() { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = boost::beast::websocket::stream< + asio::ssl::stream + >; + + error_code ec; + asio::ssl::context tls_context(asio::ssl::context::tls_client); + tls_context.add_certificate_authority(asio::buffer(spacetime_ca), ec); + tls_context.set_verify_mode(asio::ssl::verify_peer); + + using client_type = mqtt_client; + client_type c(ioc, "", std::move(tls_context)); + + c.credentials("test-qos1-websocket-tls", "", "") + .brokers("iot.fcluster.mireo.hr/mqtt", 8884) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos1!", + retain_e::no, publish_props{}, + [&c](error_code ec, reason_code rc, puback_props) { + fmt::print( + "[Test-publish-qos1-websocket-tls] " + "error_code: {}, reason_code: {}\n", ec.message(), rc.message() + ); + c.cancel(); + } + ); + + ioc.run(); + return; +} + +void publish_qos2_websocket_tls() { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = boost::beast::websocket::stream< + asio::ssl::stream + >; + + error_code ec; + asio::ssl::context tls_context(asio::ssl::context::tls_client); + tls_context.add_certificate_authority(asio::buffer(spacetime_ca), ec); + tls_context.set_verify_mode(asio::ssl::verify_peer); + + using client_type = mqtt_client; + client_type c(ioc, "", std::move(tls_context)); + + c.credentials("test-qos2-websocket-tls", "", "") + .brokers("iot.fcluster.mireo.hr/mqtt", 8884) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + c.async_publish( + "test/mqtt-test", "hello world with qos2!", + retain_e::no, publish_props{}, + [&c](error_code ec, reason_code rc, pubcomp_props) { + fmt::print( + "[Test-publish-qos2-websocket-tls] " + "error_code: {}, reason_code: {}\n", ec.message(), rc.message() + ); + c.cancel(); + } + ); + + ioc.run(); + return; +} + + +void subscribe_and_receive_websocket_tls(int num_receive) { + using namespace async_mqtt5; + asio::io_context ioc; + + using stream_type = boost::beast::websocket::stream< + asio::ssl::stream + >; + + error_code ec; + asio::ssl::context tls_context(asio::ssl::context::tls_client); + tls_context.add_certificate_authority(asio::buffer(spacetime_ca), ec); + tls_context.set_verify_mode(asio::ssl::verify_peer); + + using client_type = mqtt_client; + client_type c(ioc, "", std::move(tls_context)); + + c.credentials("test-subscriber-websocket-tls", "", "") + .brokers("iot.fcluster.mireo.hr/mqtt", 8884) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + std::vector topics; + topics.push_back(subscribe_topic{ + "test/mqtt-test", { + qos_e::exactly_once, + subscribe_options::no_local_e::no, + subscribe_options::retain_as_published_e::retain, + subscribe_options::retain_handling_e::send + } + }); + + c.async_subscribe( + topics, subscribe_props{}, + [](error_code ec, std::vector codes, suback_props) { + if (ec == asio::error::operation_aborted) + return; + fmt::print( + "[Test-subscribe-and-receive-websocket-tls] " + " error_code: {}, reason_code: {}\n", ec.message(), codes[0].message() + ); + } + ); + + for (auto i = 0; i < num_receive; i++) { + c.async_receive( + [&c, i, num_receive] ( + error_code ec, std::string topic, + std::string payload, publish_props + ) { + if (ec == asio::error::operation_aborted) + return; + fmt::print( + "[Test-subscribe-and-receive-websocket-tls] message {}/{}:" + "ec: {}, topic: {}, payload: {}\n", + i + 1, num_receive, ec.message(), topic, payload + ); + + if (i == num_receive - 1) + c.cancel(); + } + ); + } + + ioc.run(); + return; +} + + +void run_websocket_tls_examples() { + publish_qos0_websocket_tls(); + publish_qos1_websocket_tls(); + publish_qos2_websocket_tls(); + subscribe_and_receive_websocket_tls(1); +} diff --git a/include/async_mqtt5.hpp b/include/async_mqtt5.hpp new file mode 100644 index 0000000..0017de7 --- /dev/null +++ b/include/async_mqtt5.hpp @@ -0,0 +1,9 @@ +#ifndef ASYNC_MQTT5_HPP +#define ASYNC_MQTT5_HPP + +#include +#include +#include +#include + +#endif // !ASYNC_MQTT5_HPP diff --git a/include/async_mqtt5/detail/async_mutex.hpp b/include/async_mqtt5/detail/async_mutex.hpp new file mode 100644 index 0000000..adcb962 --- /dev/null +++ b/include/async_mqtt5/detail/async_mutex.hpp @@ -0,0 +1,210 @@ +#ifndef ASYNC_MQTT5_ASYNC_MUTEX_HPP +#define ASYNC_MQTT5_ASYNC_MUTEX_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; +using error_code = boost::system::error_code; + +class async_mutex { +public: + using executor_type = asio::any_io_executor; +private: + using queued_op_t = asio::any_completion_handler< + void (boost::system::error_code) + >; + using queue_t = detail::ring_buffer; + + // Handler with assigned tracking executor. + // Objects of this type are type-erased by any_completion_handler + // and stored in the waiting queue. + template + class tracked_op { + tracking_type _executor; + std::decay_t _handler; + public: + tracked_op(Handler&& h) : + _executor(tracking_executor(h)), + _handler(std::move(h)) + {} + + tracked_op(tracked_op&&) noexcept = default; + tracked_op(const tracked_op&) = delete; + + using executor_type = tracking_type; + executor_type get_executor() const noexcept { + return _executor; + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + using cancellation_slot_type = + asio::associated_cancellation_slot_t; + cancellation_slot_type get_cancellation_slot() const noexcept { + return asio::get_associated_cancellation_slot(_handler); + } + + void operator()(boost::system::error_code ec) { + std::move(_handler)(ec); + } + }; + + // Per-operation cancellation helper. + // It is safe to emit the cancellation signal from any thread + // provided there are no other concurrent calls to the async_mutex. + // The helper stores queue iterator to operation since the iterator + // would not be invalidated by other queue operations. + class cancel_waiting_op { + async_mutex& _owner; + queue_t::iterator _ihandler; + public: + cancel_waiting_op(async_mutex& owner, queue_t::iterator ih) : + _owner(owner), _ihandler(ih) + {} + + void operator()(asio::cancellation_type_t type) { + if (type == asio::cancellation_type_t::none) + return; + std::unique_lock l { _owner._thread_mutex }; + if (*_ihandler) { + auto h = std::move(*_ihandler); + auto ex = asio::get_associated_executor(h); + l.unlock(); + asio::require(ex, asio::execution::blocking.possibly) + .execute([h = std::move(h)]() mutable { + std::move(h)(asio::error::operation_aborted); + }); + } + } + }; + + spinlock _thread_mutex; + std::atomic _locked { false }; + queue_t _waiting; + executor_type _ex; + +public: + template + async_mutex(Executor&& ex) : _ex(std::forward(ex)) {} + + async_mutex(const async_mutex&) = delete; + async_mutex& operator=(const async_mutex&) = delete; + + ~async_mutex() { + cancel(); + } + + const executor_type& get_executor() const noexcept { + return _ex; + } + + bool is_locked() const noexcept { + return _locked.load(std::memory_order_relaxed); + } + + // Schedules mutex for lock operation and return immediately. + // Calls given completion handler when mutex is locked. + // It's the responsibility of the completion handler to unlock the mutex. + template + decltype(auto) lock(CompletionToken&& token) noexcept { + auto initiation = [this] (auto handler) { + this->execute_or_queue(std::move(handler)); + }; + return asio::async_initiate( + std::move(initiation), token + ); + } + + // Unlocks the mutex. The mutex must be in locked state. + // Next queued operation, if any, will be executed in a manner + // equivalent to asio::post. + void unlock() { + std::unique_lock l { _thread_mutex }; + if (_waiting.empty()) { + _locked.store(false, std::memory_order_release); + return; + } + while (!_waiting.empty()) { + auto op = std::move(_waiting.front()); + _waiting.pop_front(); + if (!op) continue; + op.get_cancellation_slot().clear(); + l.unlock(); + execute_op(std::move(op)); + break; + } + } + + // Cancels all outstanding operations waiting on the mutex. + void cancel() { + std::unique_lock l { _thread_mutex }; + + while (!_waiting.empty()) { + auto op = std::move(_waiting.front()); + _waiting.pop_front(); + if (!op) continue; + op.get_cancellation_slot().clear(); + auto ex = asio::get_associated_executor(op, _ex); + asio::require(ex, asio::execution::blocking.possibly) + .execute([op = std::move(op)]() mutable { + std::move(op)(asio::error::operation_aborted); + }); + } + } + +private: + + // Schedule operation to `opex` executor using `_ex` executor. + // The operation is equivalent to asio::post(_ex, op) but + // for some reason this form of execution is much faster. + void execute_op(queued_op_t op) { + asio::require(_ex, asio::execution::blocking.never) + .execute([ex = _ex, op = std::move(op)]() mutable { + auto opex = asio::get_associated_executor(op, ex); + opex.execute( + [op = std::move(op)]() mutable { op(error_code{}); } + ); + }); + } + + // Executes operation immediatelly if mutex is not locked + // or queues it for later execution otherwise. In both cases + // the operation will be executed in a manner equivalent + // to asio::post to avoid recursion. + void execute_or_queue(auto handler) noexcept { + std::unique_lock l { _thread_mutex }; + tracked_op h { std::move(handler) }; + if (_locked.load(std::memory_order_relaxed)) { + _waiting.emplace_back(std::move(h)); + auto slot = _waiting.back().get_cancellation_slot(); + if (slot.is_connected()) + slot.template emplace( + *this, _waiting.end() - 1 + ); + } + else { + _locked.store(true, std::memory_order_release); + l.unlock(); + execute_op(queued_op_t { std::move(h) }); + } + } +}; + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_ASYNC_MUTEX_HPP diff --git a/include/async_mqtt5/detail/async_traits.hpp b/include/async_mqtt5/detail/async_traits.hpp new file mode 100644 index 0000000..a2cc964 --- /dev/null +++ b/include/async_mqtt5/detail/async_traits.hpp @@ -0,0 +1,161 @@ +#ifndef ASYNC_MQTT5_ASYNC_TRAITS_HPP +#define ASYNC_MQTT5_ASYNC_TRAITS_HPP + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace async_mqtt5 { + +namespace asio = boost::asio; + +// TODO: move tls_handshake_type and assign_tls_sni to +// separate header + +template +struct tls_handshake_type {}; + +template +void assign_tls_sni(const authority_path& ap, TlsContext& ctx, TlsStream& s); + +namespace detail { + +template +decltype(auto) tracking_executor(const Handler& handler) { + return asio::prefer( + asio::get_associated_executor(handler), + asio::execution::outstanding_work.tracked + ); +} + +template +using tracking_type = std::decay_t< + decltype(tracking_executor(std::declval())) +>; + +template +concept has_async_write = requires(T a) { + a.async_write( + std::declval(), + [](boost::system::error_code, size_t) {} + ); +}; + +template +concept has_tls_handshake = requires(T a) { + a.async_handshake( + typename T::handshake_type{}, + [](error_code) {} + ); +}; + +template +concept has_ws_handshake = requires(T a) { + a.async_handshake( + std::declval(), + std::declval(), + [](error_code) {} + ); +}; + +template +concept has_tls_context = requires(T a) { + a.tls_context(); +}; + +template +concept has_next_layer = requires(T a) { + a.next_layer(); +}; + +template +struct next_layer_type { + using type = T; +}; + +template +requires has_next_layer +struct next_layer_type { + using type = typename std::remove_reference_t::next_layer_type; +}; + +template +requires (!has_next_layer) +typename next_layer_type::type& next_layer(T&& a) { + return a; +} + +template +requires has_next_layer +typename next_layer_type::type& next_layer(T&& a) { + return a.next_layer(); +} + +template +using lowest_layer_type = typename boost::beast::lowest_layer_type; + +template +lowest_layer_type& lowest_layer(S&& a) { + return boost::beast::get_lowest_layer(std::forward(a)); +} + +template +struct has_tls_layer_impl : std::false_type {}; + +template +requires has_tls_handshake +struct has_tls_layer_impl : std::true_type {}; + +template +requires (!has_tls_handshake && has_next_layer) +struct has_tls_layer_impl : has_tls_layer_impl< + std::remove_cvref_t().next_layer())> +> {}; + +template +concept has_tls_layer = has_tls_layer_impl>::value; + +//TODO: move to appropriate place +template < + typename Stream, + typename ConstBufferSequence, + typename CompletionToken +> +decltype(auto) async_write( + Stream& stream, const ConstBufferSequence& buff, CompletionToken&& token +) { + // TODO: find layer that has async write method + if constexpr (has_async_write) + return stream.async_write( + buff, std::forward(token) + ); + else + return asio::async_write( + stream, buff, std::forward(token) + ); +} + +template +void setup_tls_sni(const authority_path& ap, TlsContext& ctx, Stream& s) { + if constexpr (has_tls_handshake) { + using tls_stream_type = Stream; + assign_tls_sni(ap, ctx, s); + } + else if constexpr (has_next_layer) { + using next_layer_type = typename Stream::next_layer_type; + setup_tls_sni(ap, ctx, next_layer(s)); + } +} + +} // end namespace detail + +} // end namespace async_mqtt5 + +#endif // !ASYNC_MQTT5_ASYNC_TRAITS_HPP diff --git a/include/async_mqtt5/detail/cancellable_handler.hpp b/include/async_mqtt5/detail/cancellable_handler.hpp new file mode 100644 index 0000000..9eb7294 --- /dev/null +++ b/include/async_mqtt5/detail/cancellable_handler.hpp @@ -0,0 +1,151 @@ +#ifndef ASYNC_MQTT5_CANCELLABLE_HANDLER_HPP +#define ASYNC_MQTT5_CANCELLABLE_HANDLER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace async_mqtt5::detail { + +template < + typename Handler, typename Executor, + typename CancelArgs = std::tuple<> +> +class cancellable_handler { + struct op_state { + std::decay_t _handler; + tracking_type _handler_ex; + cancellable_handler* _owner; + + op_state(Handler&& handler, cancellable_handler* owner) : + _handler(std::move(handler)), + _handler_ex(tracking_executor(_handler)), + _owner(owner) + {} + + void cancel_op(asio::cancellation_type_t ct) { + if (ct != asio::cancellation_type_t::none) + _owner->cancel(); + } + }; + + struct cancel_proxy { + std::weak_ptr _state_weak_ptr; + Executor _executor; + + cancel_proxy(std::shared_ptr state, const Executor& ex) : + _state_weak_ptr(std::move(state)), _executor(ex) + {} + + void operator()(asio::cancellation_type_t type) { + auto op = []( + std::weak_ptr wptr, + asio::cancellation_type_t type + ) { + if (auto state = wptr.lock()) + state->cancel_op(type); + }; + asio::dispatch( + _executor, + asio::prepend(std::move(op), _state_weak_ptr, type) + ); + } + }; + + std::shared_ptr _state; + Executor _executor; + +public: + cancellable_handler(Handler&& handler, const Executor& ex) { + auto alloc = asio::get_associated_allocator(handler); + _state = std::allocate_shared( + alloc, std::move(handler),this + ); + _executor = ex; + auto slot = asio::get_associated_cancellation_slot(_state->_handler); + if (slot.is_connected()) + slot.template emplace(_state, ex); + } + + cancellable_handler(cancellable_handler&& other) noexcept : + _state(std::exchange(other._state, nullptr)), + _executor(std::move(other._executor)) + { + if (!empty()) + _state->_owner = this; + } + + cancellable_handler(const cancellable_handler&) = delete; + + ~cancellable_handler() { + cancel(); + } + + bool empty() const noexcept { + return _state == nullptr; + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_state->_handler); + } + + void cancel() { + if (empty()) return; + + auto h = std::move(_state->_handler); + _state.reset(); + asio::get_associated_cancellation_slot(h).clear(); + + auto op = std::apply([&h](auto... args) { + return asio::prepend( + std::move(h), asio::error::operation_aborted, args... + ); + }, CancelArgs {}); + + asio::dispatch(std::move(_executor), std::move(op)); + } + + template + void complete(Args&&... args) { + if (empty()) return; + + auto h = std::move(_state->_handler); + _state.reset(); + asio::get_associated_cancellation_slot(h).clear(); + + asio::dispatch( + std::move(_executor), + asio::prepend(std::move(h), std::forward(args)...) + ); + } + + template + void complete_post(Args&&... args) { + if (empty()) return; + + auto h = std::move(_state->_handler); + _state.reset(); + asio::get_associated_cancellation_slot(h).clear(); + + asio::post( + std::move(_executor), + asio::prepend(std::move(h), std::forward(args)...) + ); + + } + +}; + +} // end async_mqtt5::detail + +#endif // !ASYNC_MQTT5_CANCELLABLE_HANDLER_HPP diff --git a/include/async_mqtt5/detail/control_packet.hpp b/include/async_mqtt5/detail/control_packet.hpp new file mode 100644 index 0000000..95b8b26 --- /dev/null +++ b/include/async_mqtt5/detail/control_packet.hpp @@ -0,0 +1,153 @@ +#ifndef ASYNC_MQTT5_CONTROL_PACKET_HPP +#define ASYNC_MQTT5_CONTROL_PACKET_HPP + +#include + +#include +#include + +#include + +namespace async_mqtt5 { + +namespace asio = boost::asio; + +enum class control_code_e : std::uint8_t { + no_packet = 0b00000000, // 0 + + connect = 0b00010000, // 1 + connack = 0b00100000, // 2 + publish = 0b00110000, // 3 + puback = 0b01000000, // 4 + pubrec = 0b01010000, // 5 + pubrel = 0b01100000, // 6 + pubcomp = 0b01110000, // 7 + subscribe = 0b10000000, // 8 + suback = 0b10010000, // 9 + unsubscribe = 0b10100000, // 10 + unsuback = 0b10110000, // 11 + pingreq = 0b11000000, // 12 + pingresp = 0b11010000, // 13 + disconnect = 0b11100000, // 14 + auth = 0b11110000, // 15 +}; + +constexpr struct with_pid_ {} with_pid {}; +constexpr struct no_pid_ {} no_pid {}; + +template +class control_packet { + uint16_t _packet_id; + + using alloc_type = Allocator; + using deleter = boost::alloc_deleter; + std::unique_ptr _packet; + + control_packet( + const Allocator& a, + uint16_t packet_id, std::string packet + ) noexcept : + _packet_id(packet_id), + _packet(boost::allocate_unique(a, std::move(packet))) + {} + +public: + control_packet(control_packet&&) noexcept = default; + control_packet(const control_packet&) noexcept = delete; + + template < + typename EncodeFun, + typename ...Args + > + static control_packet of( + with_pid_, const Allocator& alloc, + EncodeFun&& encode, uint16_t packet_id, Args&&... args + ) { + return control_packet { + alloc, packet_id, encode(packet_id, std::forward(args)...) + }; + } + + template < + typename EncodeFun, + typename ...Args + > + static control_packet of( + no_pid_, const Allocator& alloc, + EncodeFun&& encode, Args&&... args + ) { + return control_packet { + alloc, 0, encode(std::forward(args)...) + }; + } + + control_code_e control_code() const { + return control_code_e(uint8_t(*(_packet->data())) & 0b11110000); + } + + uint16_t packet_id() const { + return _packet_id; + } + + qos_e qos() const { + assert(control_code() == control_code_e::publish); + auto byte = (uint8_t(*(_packet->data())) & 0b00000110) >> 1; + return qos_e(byte); + } + + control_packet& set_dup() { + assert(control_code() == control_code_e::publish); + auto& byte = *(_packet->data()); + byte |= 0b00001000; + return *this; + } + + const std::string& wire_data() const { + return *_packet; + } +}; + +class packet_id_allocator { + std::mutex _mtx; + boost::container::flat_map _free_ids; + static constexpr uint16_t MAX_PACKET_ID = 65535; + +public: + packet_id_allocator() { + _free_ids.emplace(1, MAX_PACKET_ID); + } + + uint16_t allocate() { + std::lock_guard _(_mtx); + if (_free_ids.empty()) return 0; + auto it = std::prev(_free_ids.end()); + auto ret = it->second; + if (it->first > --it->second) + _free_ids.erase(it); + return ret; + } + + void free(uint16_t pid) { + std::lock_guard _(_mtx); + auto it = _free_ids.upper_bound(pid); + uint16_t* end_p = nullptr; + if (it != _free_ids.begin()) { + auto pit = std::prev(it); + if (pit->second + 1 == pid) + end_p = &pit->second; + } + auto end_v = pid; + if (it != _free_ids.end() && pid + 1 == it->first) { + end_v = it->second; + _free_ids.erase(it); + } + if (!end_p) + _free_ids.emplace(pid, end_v); + else + *end_p = end_v; + } +}; + +} // end namespace async_mqtt5 + +#endif // !ASYNC_MQTT5_CONTROL_PACKET_HPP diff --git a/include/async_mqtt5/detail/internal_types.hpp b/include/async_mqtt5/detail/internal_types.hpp new file mode 100644 index 0000000..e66df83 --- /dev/null +++ b/include/async_mqtt5/detail/internal_types.hpp @@ -0,0 +1,62 @@ +#ifndef ASYNC_MQTT5_INTERNAL_TYPES_HPP +#define ASYNC_MQTT5_INTERNAL_TYPES_HPP + +#include +#include + +#include + +namespace async_mqtt5::detail { + +using byte_citer = std::string::const_iterator; + +using time_stamp = std::chrono::time_point; +using duration = time_stamp::duration; + +struct credentials { + std::string client_id; + std::optional username; + std::optional password; + + credentials() = default; + credentials( + std::string client_id, + std::string username, std::string password + ) : + client_id(std::move(client_id)) + { + if (!username.empty()) + this->username = std::move(username); + if (!password.empty()) + this->password = std::move(password); + } +}; + +struct mqtt_context { + credentials credentials; + std::optional will; + connect_props co_props; + connack_props ca_props; +}; + +struct disconnect_context { + disconnect_rc_e reason_code = disconnect_rc_e::normal_disconnection; + disconnect_props props = {}; + bool terminal = false; +}; + +using serial_num_t = uint32_t; +constexpr serial_num_t no_serial = 0; + +namespace send_flag { + +constexpr unsigned none = 0b000; +constexpr unsigned throttled = 0b001; +constexpr unsigned prioritized = 0b010; +constexpr unsigned terminal = 0b100; + +}; + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_INTERNAL_TYPES_HPP diff --git a/include/async_mqtt5/detail/ring_buffer.hpp b/include/async_mqtt5/detail/ring_buffer.hpp new file mode 100644 index 0000000..ac7faab --- /dev/null +++ b/include/async_mqtt5/detail/ring_buffer.hpp @@ -0,0 +1,416 @@ +#ifndef ASYNC_MQTT5_RING_BUFFER_HPP +#define ASYNC_MQTT5_RING_BUFFER_HPP + +#include + +namespace async_mqtt5::detail { + +/* + +Best used LIFO queues. +It supports random access iterators, constant time insert +and erase operations at the beginning or the end of the +buffer and interoperability with std algorithms. Buffer +capacity is ensured to be power of 2. + +*/ + +template > +class ring_buffer { +public: +/* + EMPTY | 1 | 2 | FULL + |-----------------|-----------------|-----------------|---------------- + | ............... | ****........... | ****.......**** | *************** + | > | [ > | > [ | ^ + | B, F == npos | F B | B F | B == F + |-----------------|-----------------|-----------------|---------------- +*/ + using value_type = T; + using allocator_type = Allocator; + using allocator_traits = std::allocator_traits; + using size_type = std::size_t; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = T*; + using const_pointer = const T*; + + template class _iter; + + using iterator = _iter; + using const_iterator = _iter; + + iterator begin() noexcept; + iterator end() noexcept; + const_iterator begin() const noexcept; + const_iterator end() const noexcept; + const_iterator cbegin() const noexcept; + const_iterator cend() const noexcept; + + ring_buffer() = default; + + template requires std::is_constructible_v + explicit ring_buffer(size_type capacity, Alloc&& allocator) : _alloc((Alloc&&)allocator) { + reserve(capacity); + } + + template requires std::is_constructible_v + explicit ring_buffer(Alloc&& allocator) : _alloc((Alloc&&)allocator) { + } + + explicit ring_buffer(size_type capacity) : ring_buffer(capacity, allocator_type { }) { + } + + ring_buffer(ring_buffer&& other) noexcept : + _buff { std::exchange(other._buff, nullptr) }, + _front { std::exchange(other._front, -1) }, + _back { std::exchange(other._back, 0) }, + _capacity { std::exchange(other._capacity, 0) }, + _alloc { other._alloc } + { + } + + ~ring_buffer() { + if (_buff) { + clear(); + _alloc.deallocate(_buff, _capacity); + } + } + + ring_buffer& operator=(ring_buffer&& other) noexcept { + if constexpr (allocator_traits::propagate_on_container_move_assignment::value) { + clear_and_exchange_with(other); + _alloc = other._alloc; + } + else { + if (_alloc == other._alloc) { + clear_and_exchange_with(other); + } + else { + clear(); + auto s = other.size(); + reserve(s); + for (size_type i = 0; i < s; ++i) { + push_back((value_type&&)other.front()); + other.pop_front(); + } + } + } + return *this; + } + + size_type capacity() const noexcept { + return _capacity; + } + + size_type size() const noexcept { + if (empty()) { + return 0; + } + return _back > _front ? _back - _front : (_capacity - _front) + _back; + } + + bool empty() const noexcept { + return _front == npos; + } + + bool full() const noexcept { + return _front == _back; + } + + reference front() noexcept { + return _buff[_front]; + } + + const_reference front() const noexcept { + return _buff[_front]; + } + + reference back() noexcept { + return _buff[index(_back - 1)]; + } + + const_reference back() const noexcept { + return _buff[index(_back - 1)]; + } + + reference operator[](size_type i) noexcept { + return _buff[index(_front + i)]; + } + + const_reference operator[](size_type i) const noexcept { + return _buff[index(_front + i)]; + } + + void pop_front() noexcept { + allocator_traits::destroy(_alloc, &_buff[_front]); + _front = index(_front + 1); + if (_front == _back) { + _front = npos; + _back = 0; + } + } + + void push_front(const value_type& v) noexcept { + grow_if_needed(); + _front = _front == npos ? index(_back - 1) : index(_front - 1); + allocator_traits::construct(_alloc, &_buff[_front], v); + } + + void push_front(value_type&& v) noexcept { + grow_if_needed(); + _front = _front == npos ? index(_back - 1) : index(_front - 1); + allocator_traits::construct(_alloc, &_buff[_front], (value_type&&)v); + } + + template + void emplace_front(Args&&... args) noexcept { + grow_if_needed(); + _front = _front == npos ? index(_back - 1) : index(_front - 1); + allocator_traits::construct(_alloc, &_buff[_front], (Args&&)args...); + } + + void push_back(const value_type& v) noexcept { + grow_if_needed(); + allocator_traits::construct(_alloc, &_buff[_back], v); + if (_front == npos) { + _front = _back; + } + _back = index(_back + 1); + } + + void push_back(value_type&& v) noexcept { + grow_if_needed(); + allocator_traits::construct(_alloc, &_buff[_back], (value_type&&)v); + if (_front == npos) { + _front = _back; + } + _back = index(_back + 1); + } + + template + void emplace_back(Args&&... args) { + grow_if_needed(); + allocator_traits::construct(_alloc, &_buff[_back], (Args&&)args...); + if (_front == npos) { + _front = _back; + } + _back = index(_back + 1); + } + + void pop_back() noexcept { + _back = index(_back - 1); + allocator_traits::destroy(_alloc, &_buff[_back]); + if (_front == _back) { + _front = npos; + _back = 0; + } + } + + void clear() noexcept { + for (size_type i = 0; i < size(); ++i) { + allocator_traits::destroy(_alloc, &_buff[index(i)]); + } + _front = npos; + _back = 0; + } + + void swap(ring_buffer& b) noexcept { + _buff = std::exchange(b._buff, _buff); + _front = std::exchange(b._front, _front); + _back = std::exchange(b._back, _back); + _capacity = std::exchange(b._capacity, _capacity); + _alloc = std::exchange(b._alloc, _alloc); //?? allocator_traits::propagate_on_container_swap::value + } + + void reserve(size_type new_capacity) noexcept { + if (new_capacity <= _capacity) { + return; + } + + if ((new_capacity & (new_capacity - 1)) != 0) { + #if defined(_MSC_VER) + unsigned long msb = 0; + _BitScanReverse(&msb, static_cast(new_capacity)); + uint32_t lz = 32u - msb - 1; + #else + uint32_t lz = __builtin_clz(new_capacity); + #endif + new_capacity = 1ull << (32u - lz); + } + + auto new_buff = _alloc.allocate(new_capacity); + auto s = size(); + if (_buff) { + for (size_type i = 0; i < s; ++i) { + auto& v = operator[](i); + allocator_traits::construct(_alloc, &new_buff[i], (value_type&&)v); + allocator_traits::destroy(_alloc, &v); + } + _alloc.deallocate(_buff, _capacity); + } + _buff = new_buff; + _back = s; + _front = _back == 0 ? npos : 0; + _capacity = new_capacity; + } + +private: + constexpr size_type index(size_type i) const noexcept { + return i & (_capacity - 1); + } + + void grow_if_needed() { + if (!_buff || full()) [[unlikely]] + reserve(_capacity == 0 ? min_capacity : _capacity * 2); + } + + void clear_and_exchange_with(ring_buffer& other) noexcept { + clear(); + _alloc.deallocate(_buff, _capacity); + _buff = std::exchange(other._buff, nullptr); + _front = std::exchange(other._front, npos); + _back = std::exchange(other._back, 0); + _capacity = std::exchange(other._capacity, 0); + } + + pointer _buff = nullptr; + size_type _front = npos; + size_type _back = 0; + size_type _capacity = 0; + [[no_unique_address]] allocator_type _alloc; + + static constexpr size_type npos = static_cast(-1); + static constexpr size_type min_capacity = 4; +}; + +template template +class ring_buffer::_iter { +public: + using value_type = T; + using pointer = P; + using reference = R; + using difference_type = std::ptrdiff_t; + + _iter() noexcept = default; + _iter(const _iter&) noexcept = default; + _iter(_iter&&) noexcept = default; + _iter& operator=(const _iter&) noexcept = default; + _iter& operator=(_iter&&) noexcept = default; + + reference operator*() const noexcept { + return *_p; + } + + pointer operator->() const noexcept { + return _p; + } + + _iter& operator++() noexcept { + _p = &_b->operator[](++_i); + return *this; + } + + _iter operator++(int) noexcept { + auto tmp = *this; + _p = &_b->operator[](++_i); + return tmp; + } + + _iter& operator--() noexcept { + _p = &_b->operator[](--_i); + return *this; + } + + _iter operator--(int) noexcept { + auto tmp = *this; + _p = &_b->operator[](++_i); + return tmp; + } + + _iter& operator+=(difference_type d) noexcept { + _p = &_b->operator[]((_i += d)); + return *this; + } + + _iter& operator-=(difference_type d) noexcept { + _p = &_b->operator[]((_i -= d)); + return *this; + } + +private: + friend class ring_buffer; + + _iter(ring_pointer b, size_type i) : _b(b), _i(i), _p(&b->operator[](i)) { + } + + ring_pointer _b = nullptr; + size_type _i = npos; + pointer _p = nullptr; + + friend bool operator==(const _iter& a, const _iter& b) noexcept { + return a._i == b._i; + } + + friend auto operator<=>(const _iter& a, const _iter& b) noexcept { + return difference_type(a._i - b._i); + } + + friend difference_type operator-(const _iter& a, const _iter& b) noexcept { + return a._i - b._i; + } + + friend _iter operator+(const _iter& a, difference_type d) noexcept { + return { a._b, a._i + d }; + } + + friend _iter operator-(const _iter& a, difference_type d) noexcept { + return { a._b, a._i - d }; + } +}; + +template +typename ring_buffer::iterator ring_buffer::begin() noexcept { + return { this, 0 }; +} + +template +typename ring_buffer::iterator ring_buffer::end() noexcept { + return { this, size() }; +} + +template +typename ring_buffer::const_iterator ring_buffer::begin() const noexcept { + return { this, 0 }; +} + +template +typename ring_buffer::const_iterator ring_buffer::end() const noexcept { + return { this, size() }; +} + +template +typename ring_buffer::const_iterator ring_buffer::cbegin() const noexcept { + return { this, 0 }; +} + +template +typename ring_buffer::const_iterator ring_buffer::cend() const noexcept { + return { this, size() }; +} + +} // namespace async_mqtt5::detail + +namespace std { + +template +void swap( + async_mqtt5::detail::ring_buffer& a, + async_mqtt5::detail::ring_buffer& b) +{ + a.swap(b); +} + +} + +#endif // !ASYNC_MQTT5_RING_BUFFER_HPP diff --git a/include/async_mqtt5/detail/spinlock.hpp b/include/async_mqtt5/detail/spinlock.hpp new file mode 100644 index 0000000..66fba92 --- /dev/null +++ b/include/async_mqtt5/detail/spinlock.hpp @@ -0,0 +1,58 @@ +#ifndef ASYNC_MQTT5_SPINLOCK_HPP +#define ASYNC_MQTT5_SPINLOCK_HPP + +#include + +namespace async_mqtt5::detail { + +#if defined(_MSC_VER) + /* prefer using intrinsics directly instead of winnt.h macro */ + /* http://software.intel.com/en-us/forums/topic/296168 */ + //#include + #if defined(_M_AMD64) || defined(_M_IX86) + #pragma intrinsic(_mm_pause) + #define __pause() _mm_pause() + /* (if pause not supported by older x86 assembler, "rep nop" is equivalent)*/ + /*#define __pause() __asm rep nop */ + #elif defined(_M_IA64) + #pragma intrinsic(__yield) + #define __pause() __yield() + #else + #define __pause() YieldProcessor() + #endif +#elif defined(__x86_64__) || defined(__i386__) + #define __pause() __asm__ __volatile__ ("pause") +#elif defined(__arm__) || defined(__arm64__) || defined(__aarch64__) + #define __pause() __asm__ __volatile__ ("yield") +#endif + +// https://rigtorp.se/spinlock/ + +class spinlock { + std::atomic lock_ { false }; +public: + void lock() noexcept { + for (;;) { + // Optimistically assume the lock is free on the first try + if (!lock_.exchange(true, std::memory_order_acquire)) + return; + // Wait for lock to be released without generating cache misses + while (lock_.load(std::memory_order_relaxed)) __pause(); + } + } + + bool try_lock() noexcept { + // First do a relaxed load to check if lock is free in order to prevent + // unnecessary cache misses if someone does while(!try_lock()) + return !lock_.load(std::memory_order_relaxed) && + !lock_.exchange(true, std::memory_order_acquire); + } + + void unlock() noexcept { + lock_.store(false, std::memory_order_release); + } +}; + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_SPINLOCK_HPP diff --git a/include/async_mqtt5/error.hpp b/include/async_mqtt5/error.hpp new file mode 100644 index 0000000..ce01623 --- /dev/null +++ b/include/async_mqtt5/error.hpp @@ -0,0 +1,423 @@ +#ifndef ASYNC_MQTT5_ERROR_HPP +#define ASYNC_MQTT5_ERROR_HPP + +#include +#include + +#include + +namespace async_mqtt5 { + + +enum class disconnect_rc_e : std::uint8_t { + normal_disconnection = 0x00, + disconnect_with_will_message = 0x04, + unspecified_error = 0x80, + malformed_packet = 0x81, + protocol_error = 0x82, + implementation_specific_error = 0x83, + topic_name_invalid = 0x90, + receive_maximum_exceeded = 0x93, + topic_alias_invalid = 0x94, + packet_too_large = 0x95, + message_rate_too_high = 0x96, + quota_exceeded = 0x97, + administrative_action = 0x98, + payload_format_invalid = 0x99 +}; + +namespace client { + + +enum class error : int { + fatal_error = 100, + malformed_packet, + pid_overrun, + reconnected, + disconnected, + + // publish + qos_not_supported, + retain_not_available, + topic_alias_maximum_reached +}; + + +inline std::string client_error_to_string(error err) { + using enum error; + + switch (err) { + case fatal_error: + return "A fatal error occurred"; + case malformed_packet: + return "Malformed packet has been received"; + case pid_overrun: + return "Ran out of the available packet ids to use"; + case reconnected: + return "The Client has reconnected"; + case disconnected: + return "The Client has been disconnected"; + case qos_not_supported: + return "The Server does not support the specified QoS"; + case retain_not_available: + return "The Server does not support retained messages."; + case topic_alias_maximum_reached: + return "The Client attempted to send a Topic Alias " + "that is greater than Topic Alias Maximum."; + default: + return "Unknown client error"; + } +} + +struct client_ec_category : public boost::system::error_category { + const char* name() const noexcept override { return "mqtt_client_error"; } + std::string message(int ev) const noexcept override { + return client_error_to_string(static_cast(ev)); + } +}; + +inline const client_ec_category& get_error_code_category() { + static client_ec_category cat; + return cat; +} + +inline boost::system::error_code make_error_code(error r) { + return { static_cast(r), get_error_code_category() }; +} + + +} // end namespace client + +namespace reason_codes { + +enum class category : uint8_t { + none, + connack, puback, pubrec, + pubrel, pubcomp, suback, + unsuback, auth, disconnect +}; + + +} // end namespace reason_codes + + +class reason_code { + uint8_t _code; + reason_codes::category _category { reason_codes::category::none }; +public: + constexpr reason_code() : _code(0xff) {} + + constexpr reason_code(uint8_t code, reason_codes::category cat) + : _code(code), _category(cat) + {} + + constexpr reason_code(uint8_t code) : _code(code) {} + + reason_code(const reason_code&) = default; + reason_code(reason_code&&) = default; + + explicit operator bool() const noexcept { + return _code >= 0x80; + } + + constexpr uint8_t value() const noexcept { + return _code; + } + + friend std::ostream& operator<<(std::ostream& os, const reason_code& rc) { + os << rc.message(); + return os; + } + + friend bool operator<(const reason_code& lhs, const reason_code& rhs) { + return lhs._code < rhs._code; + } + + friend bool operator==(const reason_code& lhs, const reason_code& rhs) { + return lhs._code == rhs._code && lhs._category == rhs._category; + } + + std::string message() const { + switch (_code) { + case 0x00: + using enum reason_codes::category; + if (_category == suback) + return "The subscription is accepted with maximum QoS sent at 0"; + if (_category == disconnect) + return "Close the connection normally"; + return "The operation completed successfully"; + case 0x01: + return "The subscription is accepted with maximum QoS sent at 1"; + case 0x02: + return "The subscription is accepted with maximum QoS sent at 2"; + case 0x04: + return "The Client wishes to disconnect with the Will Message"; + case 0x10: + return "The message is accepted but there are no subscribers"; + case 0x11: + return "No matching Topic Filter is being used by the Client."; + case 0x18: + return "Continue the authentication with another step"; + case 0x19: + return "Initiate a re-authentication"; + case 0x80: + return "Unspecified error occurred"; + case 0x81: + return "Data within the packet could not be correctly parsed"; + case 0x82: + return "Data in the packet does not conform to this specification"; + case 0x83: + return "The packet is valid but not accepted by this Server"; + case 0x84: + return "The Server does not support the requested " + "version of the MQTT protocol"; + case 0x85: + return "The Client ID is valid but not allowed by this Server"; + case 0x86: + return "The Server does not accept the User Name or Password provided"; + case 0x87: + return "The request is not authorized"; + case 0x88: + return "The MQTT Server is not available"; + case 0x89: + return "The MQTT Server is busy, try again later"; + case 0x8a: + return "The Client has been banned by administrative action"; + case 0x8b: + return "The Server is shutting down"; + case 0x8c: + return "The authentication method is not supported or " + "does not match the method currently in use"; + case 0x8d: + return "No packet has been received for 1.5 times the Keepalive time"; + case 0x8e: + return "Another Connection using the same ClientID has connected " + "causing this Connection to be closed"; + case 0x8f: + return "The Topic Name is not malformed, but it is not accepted"; + case 0x90: + return "The Topic Name is not malformed, but it is not accepted"; + case 0x91: + return "The Packet Identifier is already in use"; + case 0x92: + return "The Packet Identifier is not known"; + case 0x93: + return "The Client or Server has received more than Receive " + "Maximum publication for which it has not sent PUBACK or PUBCOMP"; + case 0x94: + return "The Client or Server received a PUBLISH packet containing " + "a Topic Alias greater than the Maximum Topic Alias"; + case 0x95: + return "The packet exceeded the maximum permissible size"; + case 0x96: + return "The received data rate is too high"; + case 0x97: + return "An implementation or administrative imposed limit has been exceeded"; + case 0x98: + return "The Connection is closed due to an administrative action"; + case 0x99: + return "The Payload does not match the specified Payload Format Indicator"; + case 0x9a: + return "The Server does not support retained messages"; + case 0x9b: + return "The Server does not support the QoS the Client specified or " + "it is greater than the Maximum QoS specified"; + case 0x9c: + return "The Client should temporarily use another server"; + case 0x9d: + return "The Client should permanently use another server"; + case 0x9e: + return "The Server does not support Shared Subscriptions for this Client"; + case 0x9f: + return "The connection rate limit has been exceeded"; + case 0xa0: + return "The maximum connection time authorized for this " + "connection has been exceeded"; + case 0xa1: + return "The Server does not support Subscription Identifiers"; + case 0xa2: + return "The Server does not support Wildcard Subscriptions"; + case 0xff: + return "No reason code"; + default: + return "Invalid reason code."; + } + } +}; + +namespace reason_codes { + +using enum category; +constexpr reason_code empty {}; + +constexpr reason_code success { 0x00 }; +constexpr reason_code normal_disconnection { 0x00, disconnect }; +constexpr reason_code granted_qos_0 { 0x00, suback }; +constexpr reason_code granted_qos_1 { 0x01 }; +constexpr reason_code granted_qos_2 { 0x02 }; +constexpr reason_code disconnect_with_will_message { 0x04 }; +constexpr reason_code no_matching_subscribers { 0x10 }; +constexpr reason_code no_subscription_existed { 0x11 }; +constexpr reason_code continue_authentication { 0x18 }; +constexpr reason_code reauthenticate { 0x19 }; + +constexpr reason_code unspecified_error { 0x80 }; +constexpr reason_code malformed_packet { 0x81 }; +constexpr reason_code protocol_error { 0x82 }; +constexpr reason_code implementation_specific_error { 0x83 }; +constexpr reason_code unsupported_protocol_version { 0x84 }; +constexpr reason_code client_id_not_valid { 0x85 }; +constexpr reason_code bad_username_or_password { 0x86 }; +constexpr reason_code not_authorized { 0x87 }; +constexpr reason_code server_unavailable { 0x88 }; +constexpr reason_code server_busy { 0x89 }; +constexpr reason_code banned { 0x8a }; +constexpr reason_code server_shutting_down { 0x8b }; +constexpr reason_code bad_authentication_method { 0x8c }; +constexpr reason_code keep_alive_timeout { 0x8d }; +constexpr reason_code session_taken_over { 0x8e }; +constexpr reason_code topic_filter_invalid { 0x8f }; +constexpr reason_code topic_name_invalid { 0x90 }; +constexpr reason_code packet_id_in_use { 0x91 }; +constexpr reason_code packet_id_not_found { 0x92 }; +constexpr reason_code receive_maximum_exceeded { 0x93 }; +constexpr reason_code topic_alias_invalid { 0x94 }; +constexpr reason_code packet_too_large { 0x95 }; +constexpr reason_code message_rate_too_high { 0x96 }; +constexpr reason_code quota_exceeded { 0x97 }; +constexpr reason_code administrative_action { 0x98 }; +constexpr reason_code payload_format_invalid { 0x99 }; +constexpr reason_code retain_not_supported { 0x9a }; +constexpr reason_code qos_not_supported { 0x9b }; +constexpr reason_code use_another_server { 0x9c }; +constexpr reason_code server_moved { 0x9d }; +constexpr reason_code shared_subscriptions_not_supported { 0x9e }; +constexpr reason_code connection_rate_exceeded { 0x9f }; +constexpr reason_code maximum_connect_time { 0xa0 }; +constexpr reason_code subscription_ids_not_supported { 0xa1 }; +constexpr reason_code wildcard_subscriptions_not_supported { 0xa2 }; + +namespace detail { + +using enum category; + +template +inline std::pair valid_codes() +requires (cat == connack) { + static reason_code valid_codes[] = { + success, unspecified_error, malformed_packet, + protocol_error, implementation_specific_error, + unsupported_protocol_version, client_id_not_valid, + bad_username_or_password, not_authorized, + server_unavailable, server_busy, banned, + bad_authentication_method, topic_name_invalid, + packet_too_large, quota_exceeded, + payload_format_invalid, retain_not_supported, + qos_not_supported, use_another_server, + server_moved, connection_rate_exceeded + }; + static size_t len = sizeof(valid_codes) / sizeof(reason_code); + return std::make_pair(valid_codes, len); +} + +template +inline std::pair valid_codes() +requires (cat == puback || cat == pubrec) { + static reason_code valid_codes[] = { + success, no_matching_subscribers, unspecified_error, + implementation_specific_error, not_authorized, + topic_name_invalid, packet_id_in_use, + quota_exceeded, payload_format_invalid + }; + static size_t len = sizeof(valid_codes) / sizeof(reason_code); + return std::make_pair(valid_codes, len); +} + +template +inline std::pair valid_codes() +requires (cat == pubrel || cat == pubcomp) { + static reason_code valid_codes[] = { + success, packet_id_not_found + }; + static size_t len = sizeof(valid_codes) / sizeof(reason_code); + return std::make_pair(valid_codes, len); +} + +template +inline std::pair valid_codes() +requires (cat == suback) { + static reason_code valid_codes[] = { + granted_qos_0, granted_qos_1, granted_qos_2, + unspecified_error, implementation_specific_error, + not_authorized, topic_filter_invalid, + packet_id_in_use, quota_exceeded, + shared_subscriptions_not_supported, + subscription_ids_not_supported, + wildcard_subscriptions_not_supported + }; + static size_t len = sizeof(valid_codes) / sizeof(reason_code); + return std::make_pair(valid_codes, len); +} + +template +inline std::pair valid_codes() +requires (cat == unsuback) { + static reason_code valid_codes[] = { + success, no_subscription_existed, + unspecified_error, implementation_specific_error, + not_authorized, topic_filter_invalid, + packet_id_in_use + }; + static size_t len = sizeof(valid_codes) / sizeof(reason_code); + return std::make_pair(valid_codes, len); +} + +template +inline std::pair valid_codes() +requires (cat == disconnect) { + static reason_code valid_codes[] = { + normal_disconnection, unspecified_error, + malformed_packet,protocol_error, + implementation_specific_error, not_authorized, + server_busy, server_shutting_down, + keep_alive_timeout, session_taken_over, + topic_filter_invalid, topic_name_invalid, + receive_maximum_exceeded, topic_alias_invalid, + packet_too_large,message_rate_too_high, + quota_exceeded, administrative_action, + payload_format_invalid, retain_not_supported, + qos_not_supported, use_another_server, + server_moved, shared_subscriptions_not_supported, + connection_rate_exceeded, maximum_connect_time, + subscription_ids_not_supported, + wildcard_subscriptions_not_supported + }; + static size_t len = sizeof(valid_codes) / sizeof(reason_code); + return std::make_pair(valid_codes, len); +} + + +} // end namespace detail +} // end namespace reason_codes + + +template +inline std::optional to_reason_code(uint8_t code) { + auto [ptr, len] = reason_codes::detail::valid_codes(); + auto it = std::lower_bound(ptr, ptr + len, reason_code(code)); + + if (it->value() == code) + return *it; + return std::nullopt; +} + +} // end namespace async_mqtt5 + +namespace boost::system { + +template <> +struct is_error_code_enum : std::true_type {}; + +} // end namespace boost::system + +#endif // !ASYNC_MQTT5_ERROR_HPP diff --git a/include/async_mqtt5/impl/assemble_op.hpp b/include/async_mqtt5/impl/assemble_op.hpp new file mode 100644 index 0000000..b7c3250 --- /dev/null +++ b/include/async_mqtt5/impl/assemble_op.hpp @@ -0,0 +1,231 @@ +#ifndef ASYNC_MQTT5_ASSEMBLE_OP_HPP +#define ASYNC_MQTT5_ASSEMBLE_OP_HPP + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +class data_span : private std::pair { + using base = std::pair; +public: + using base::base; + + auto first() const { + return base::first; + } + auto last() const { + return base::second; + } + void expand_suffix(size_t num_chars) { + base::second += num_chars; + } + void remove_prefix(size_t num_chars) { + base::first += num_chars; + } + size_t size() const { + return std::distance(base::first, base::second); + } +}; + + +template +class assemble_op { + using client_service = ClientService; + struct on_read {}; + + static constexpr size_t max_packet_size = 65536; + + client_service& _svc; + std::decay_t _handler; + + std::string& _read_buff; + data_span& _data_span; + +public: + assemble_op( + client_service& svc, Handler&& handler, + std::string& read_buff, data_span& active_span + ) : + _svc(svc), + _handler(std::move(handler)), + _read_buff(read_buff), _data_span(active_span) + {} + + assemble_op(assemble_op&&) noexcept = default; + assemble_op(const assemble_op&) = delete; + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc.get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + template + void perform(duration wait_for, CompletionCondition cc) { + _read_buff.erase( + _read_buff.cbegin(), _data_span.first() + ); + // TODO: respect max packet size from CONNACK + _read_buff.resize(max_packet_size); + _data_span = { + _read_buff.cbegin(), + _read_buff.cbegin() + _data_span.size() + }; + + if (cc(error_code {}, 0) == 0 && _data_span.size()) { + return asio::post( + asio::prepend( + std::move(*this), on_read {}, error_code {}, + 0, wait_for, std::move(cc) + ) + ); + } + + // Must be evaluated before this is moved + auto store_begin = _read_buff.data() + _data_span.size(); + auto store_size = std::distance(_data_span.last(), _read_buff.cend()); + + _svc._stream.async_read_some( + asio::buffer(store_begin, store_size), wait_for, + asio::prepend( + asio::append(std::move(*this), wait_for, std::move(cc)), + on_read {} + ) + ); + } + + template + void operator()( + on_read, error_code ec, size_t bytes_read, + duration wait_for, CompletionCondition cc + ) { + if (ec == asio::error::try_again) { + _svc._async_sender.resend(); + _data_span = { _read_buff.cend(), _read_buff.cend() }; + return perform(wait_for, std::move(cc)); + } + + if (ec) + return complete(ec, 0, 0, {}, {}); + + _data_span.expand_suffix(bytes_read); + assert(_data_span.size()); + + auto control_code = uint8_t(*_data_span.first()); + + if ((control_code & 0b11110000) == 0) + // close the connection, cancel + return complete(client::error::malformed_packet, 0, 0, {}, {}); + + auto first = _data_span.first() + 1; + auto varlen = decoders::type_parse( + first, _data_span.last(), decoders::basic::varint_ + ); + + if (!varlen) { + if (_data_span.size() < 5) + return perform(wait_for, asio::transfer_at_least(1)); + return complete(client::error::malformed_packet, 0, 0, {}, {}); + } + + // TODO: respect max packet size which could be dinamically set by the broker + if (*varlen > max_packet_size - std::distance(_data_span.first(), first)) + return complete(client::error::malformed_packet, 0, 0, {}, {}); + + if (std::distance(first, _data_span.last()) < *varlen) + return perform(wait_for, asio::transfer_at_least(1)); + + _data_span.remove_prefix( + std::distance(_data_span.first(), first) + *varlen + ); + + dispatch(wait_for, control_code, first, first + *varlen); + } + +private: + static bool valid_header(uint8_t control_byte) { + using enum control_code_e; + + auto code = control_code_e(control_byte & 0b11110000); + + if (code == publish) + return true; + + auto res = control_byte & 0b00001111; + if (code == pubrel) + return res == 0b00000010; + return res == 0b00000000; + } + + static bool contains_packet_id(control_code_e code) { + using enum control_code_e; + + return code == puback || code == pubrec + || code == pubrel || code == pubcomp + || code == subscribe || code == suback + || code == unsubscribe || code == unsuback; + } + + void dispatch( + duration wait_for, + uint8_t control_code, byte_citer first, byte_citer last + ) { + using namespace decoders; + using enum control_code_e; + + if (!valid_header(control_code)) + return complete(client::error::malformed_packet, 0, 0, {}, {}); + + auto code = control_code_e(control_code & 0b11110000); + + if (code == pingresp) + return perform(wait_for, asio::transfer_at_least(0)); + + uint16_t packet_id = 0; + if (contains_packet_id(code)) + packet_id = decoders::decode_packet_id(first).value(); + + bool is_reply = code != publish && code != auth && code != disconnect; + if (is_reply) { + _svc._replies.dispatch(error_code {}, code, packet_id, first, last); + return perform(wait_for, asio::transfer_at_least(0)); + } + + complete(error_code {}, packet_id, control_code, first, last); + } + + void complete( + error_code ec, uint16_t packet_id, uint8_t control_code, + byte_citer first, byte_citer last + ) { + asio::dispatch( + get_executor(), + asio::prepend( + std::move(_handler), ec, packet_id, control_code, + first, last + ) + ); + } +}; + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_ASSEMBLE_OP_HPP diff --git a/include/async_mqtt5/impl/async_sender.hpp b/include/async_mqtt5/impl/async_sender.hpp new file mode 100644 index 0000000..99e2061 --- /dev/null +++ b/include/async_mqtt5/impl/async_sender.hpp @@ -0,0 +1,235 @@ +#ifndef ASYNC_MQTT5_ASYNC_SENDER_HPP +#define ASYNC_MQTT5_ASYNC_SENDER_HPP + +#include +#include +#include +#include + +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +class write_req { + static constexpr unsigned SERIAL_BITS = sizeof(serial_num_t) * 8; + + asio::const_buffer _buffer; + serial_num_t _serial_num; + unsigned _flags; + asio::any_completion_handler _handler; + +public: + write_req( + asio::const_buffer buffer, + serial_num_t serial_num, unsigned flags, + asio::any_completion_handler handler + ) : _buffer(buffer), _serial_num(serial_num), _flags(flags), + _handler(std::move(handler)) {} + + static serial_num_t next_serial_num(serial_num_t last) { + return ++last; + } + + asio::const_buffer buffer() const { return _buffer; } + void complete(error_code ec) { std::move(_handler)(ec); } + bool throttled() const { return _flags & send_flag::throttled; } + bool terminal() const { return _flags & send_flag::terminal; } + + bool operator<(const write_req& other) const { + if (prioritized() != other.prioritized()) { + return prioritized(); + } + + auto s1 = _serial_num; + auto s2 = other._serial_num; + + if (s1 < s2) + return (s2 - s1) < (1 << (SERIAL_BITS - 1)); + return (s1 - s2) >= (1 << (SERIAL_BITS - 1)); + } + +private: + bool prioritized() const { return _flags & send_flag::prioritized; } +}; + +template +class async_sender { + using client_service = ClientService; + + using queue_allocator_type = asio::recycling_allocator; + using write_queue_t = std::vector; + + ClientService& _svc; + write_queue_t _write_queue; + bool _write_in_progress { false }; + + static constexpr uint16_t MAX_LIMIT = 65535; + uint16_t _limit { MAX_LIMIT }; + uint16_t _quota { MAX_LIMIT }; + + serial_num_t _last_serial_num { 0 }; + +public: + async_sender(ClientService& svc) : _svc(svc) {} + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc.get_executor(); + } + + using allocator_type = queue_allocator_type; + allocator_type get_allocator() const noexcept { + return allocator_type {}; + } + + serial_num_t next_serial_num() { + return _last_serial_num = write_req::next_serial_num(_last_serial_num); + } + + template + decltype(auto) async_send( + const BufferType& buffer, + serial_num_t serial_num, unsigned flags, + CompletionToken&& token + ) { + auto initiation = [this]( + auto handler, const BufferType& buffer, + serial_num_t serial_num, unsigned flags + ) { + _write_queue.emplace_back( + asio::buffer(buffer), serial_num, flags, std::move(handler) + ); + do_write(); + }; + + return asio::async_initiate( + std::move(initiation), token, buffer, serial_num, flags + ); + } + + void cancel() { + auto ops = std::move(_write_queue); + for (auto& op : ops) + op.complete(asio::error::operation_aborted); + } + + void resend() { + if (_write_in_progress) + return; + + // The _write_in_progress flag is set to true to prevent any write + // operations executing before the _write_queue is filled with + // all the packets that require resending. + _write_in_progress = true; + + auto new_limit = _svc._stream_context.connack_prop(prop::receive_maximum); + _limit = new_limit.value_or(MAX_LIMIT); + _quota = _limit; + + auto write_queue = std::move(_write_queue); + _svc._replies.resend_unanswered(); + + for (auto& op : write_queue) + op.complete(asio::error::try_again); + + std::stable_sort(_write_queue.begin(), _write_queue.end()); + + _write_in_progress = false; + do_write(); + } + + void operator()(write_queue_t write_queue, error_code ec, size_t) { + _write_in_progress = false; + + if (ec == asio::error::try_again) { + _write_queue.insert( + _write_queue.begin(), + std::make_move_iterator(write_queue.begin()), + std::make_move_iterator(write_queue.end()) + ); + return resend(); + } + + // errors, if any, are propagated to ops + for (auto& op : write_queue) + op.complete(ec); + + if ( + ec == asio::error::operation_aborted || + ec == asio::error::no_recovery + ) + return; + + do_write(); + } + + void throttled_op_done() { + if (_limit == MAX_LIMIT) + return; + + ++_quota; + do_write(); + } + +private: + void do_write() { + if (_write_in_progress || _write_queue.empty()) + return; + + _write_in_progress = true; + + write_queue_t write_queue; + + auto terminal_req = std::find_if( + _write_queue.begin(), _write_queue.end(), + [](const auto& op) { return op.terminal(); } + ); + if (terminal_req != _write_queue.end()) { + write_queue.push_back(std::move(*terminal_req)); + _write_queue.erase(terminal_req); + } + else if (_limit == MAX_LIMIT) + write_queue = std::move(_write_queue); + else { + auto throttled_ptr = std::stable_partition( + _write_queue.begin(), _write_queue.end(), + [](const auto& op) { return !op.throttled(); } + ); + uint16_t dist = std::distance(throttled_ptr, _write_queue.end()); + uint16_t throttled_num = std::min(dist, _quota); + _quota -= throttled_num; + throttled_ptr += throttled_num; + + if (throttled_ptr == _write_queue.begin()) { + _write_in_progress = false; + return; + } + + write_queue.insert( + write_queue.end(), + std::make_move_iterator(_write_queue.begin()), + std::make_move_iterator(throttled_ptr) + ); + _write_queue.erase(_write_queue.begin(), throttled_ptr); + } + + std::vector buffers; + buffers.reserve(write_queue.size()); + for (const auto& op : write_queue) + buffers.push_back(op.buffer()); + + _svc._replies.clear_fast_replies(); + + _svc._stream.async_write( + buffers, + asio::prepend(std::ref(*this), std::move(write_queue)) + ); + } + +}; + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_ASYNC_SENDER_HPP diff --git a/include/async_mqtt5/impl/autoconnect_stream.hpp b/include/async_mqtt5/impl/autoconnect_stream.hpp new file mode 100644 index 0000000..3aa99e2 --- /dev/null +++ b/include/async_mqtt5/impl/autoconnect_stream.hpp @@ -0,0 +1,196 @@ +#ifndef ASYNC_MQTT5_AUTOCONNECT_STREAM_HPP +#define ASYNC_MQTT5_AUTOCONNECT_STREAM_HPP + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template < + typename StreamType, + typename StreamContext = std::monostate +> +class autoconnect_stream { +public: + using stream_type = StreamType; + using stream_context_type = StreamContext; + using executor_type = typename stream_type::executor_type; +private: + using stream_ptr = std::shared_ptr; + + executor_type _stream_executor; + async_mutex _conn_mtx; + asio::steady_timer _read_timer, _connect_timer; + endpoints _endpoints; + + stream_ptr _stream_ptr; + stream_context_type& _stream_context; + + template + friend class reconnect_op; + + template + friend class read_op; + + template + friend class write_op; + + template + friend class disconnect_op; + +public: + autoconnect_stream( + const executor_type& ex, stream_context_type& context + ) : + _stream_executor(ex), + _conn_mtx(_stream_executor), + _read_timer(_stream_executor), _connect_timer(_stream_executor), + _endpoints(_stream_executor, _connect_timer), + _stream_context(context) + { + replace_next_layer(construct_next_layer()); + } + + using next_layer_type = stream_type; + next_layer_type& next_layer() { + return *_stream_ptr; + } + + const next_layer_type& next_layer() const { + return *_stream_ptr; + } + + executor_type get_executor() const noexcept { + return _stream_executor; + } + + void brokers(std::string hosts, uint16_t default_port) { + _endpoints.brokers(std::move(hosts), default_port); + } + + bool is_open() const noexcept { + return lowest_layer(*_stream_ptr).is_open(); + } + + void open() { + error_code ec; + lowest_layer(*_stream_ptr).open(asio::ip::tcp::v4(), ec); + } + + void cancel() { + error_code ec; + lowest_layer(*_stream_ptr).cancel(ec); + } + + void close() { + error_code ec; + shutdown(asio::ip::tcp::socket::shutdown_both); + lowest_layer(*_stream_ptr).close(ec); + _connect_timer.cancel(); + } + + void shutdown(asio::ip::tcp::socket::shutdown_type what) { + error_code ec; + lowest_layer(*_stream_ptr).shutdown(what, ec); + } + + bool was_connected() const { + error_code ec; + lowest_layer(*_stream_ptr).remote_endpoint(ec); + return ec == boost::system::errc::success; + } + + template + decltype(auto) async_read_some( + const BufferType& buffer, duration wait_for, CompletionToken&& token + ) { + auto initiation = [this]( + auto handler, const BufferType& buffer, duration wait_for + ) { + read_op { *this, std::move(handler) } + .perform(buffer, wait_for); + }; + + return asio::async_initiate( + std::move(initiation), token, + buffer, wait_for + ); + } + + template + decltype(auto) async_write( + const BufferType& buffer, CompletionToken&& token + ) { + auto initiation = [this]( + auto handler, const BufferType& buffer + ) { + write_op { *this, std::move(handler) }.perform(buffer); + }; + + return asio::async_initiate( + std::move(initiation), token, buffer + ); + } + +private: + stream_ptr construct_next_layer() const { + stream_ptr sptr; + if constexpr (has_tls_context) + sptr = std::make_shared( + _stream_executor, _stream_context.tls_context() + ); + else + sptr = std::make_shared(_stream_executor); + + error_code ec; + lowest_layer(*sptr).set_option( + asio::socket_base::reuse_address(true), ec + ); + + return sptr; + } + + void replace_next_layer(stream_ptr sptr) { + // close() will cancel all outstanding async operations on + // _stream_ptr; cancelling posts operation_aborted to handlers + // but handlers will be executed after std::exchange below; + // handlers should therefore treat (operation_aborted && is_open()) + // equivalent to try_again. + + if (_stream_ptr) + close(); + std::exchange(_stream_ptr, std::move(sptr)); + } + + template + decltype(auto) async_reconnect(stream_ptr s, CompletionToken&& token) { + auto initiation = [this](auto handler, stream_ptr s) { + reconnect_op { *this, std::move(handler) }.perform(s); + }; + + return asio::async_initiate( + std::move(initiation), token, s + ); + } +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_AUTOCONNECT_STREAM_HPP diff --git a/include/async_mqtt5/impl/client_service.hpp b/include/async_mqtt5/impl/client_service.hpp new file mode 100644 index 0000000..c53c369 --- /dev/null +++ b/include/async_mqtt5/impl/client_service.hpp @@ -0,0 +1,278 @@ +#ifndef ASYNC_MQTT5_CLIENT_SERVICE_HPP +#define ASYNC_MQTT5_CLIENT_SERVICE_HPP + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace async_mqtt5::detail { + + +template +class stream_context; + +template +requires has_tls_layer +class stream_context { + using tls_context_type = TlsContext; + + mqtt_context _mqtt_context; + tls_context_type _tls_context; +public: + stream_context(TlsContext tls_context) : + _tls_context(std::move(tls_context)) + {} + + mqtt_context& mqtt_context() { + return _mqtt_context; + } + + TlsContext& tls_context() { + return _tls_context; + } + + void will(will will) { + _mqtt_context.will = std::move(will); + } + + template + auto connack_prop(Prop p) { + return _mqtt_context.ca_props[p]; + } + + void credentials( + std::string client_id, + std::string username = "", std::string password = "" + ) { + _mqtt_context.credentials = { + std::move(client_id), + std::move(username), std::move(password) + }; + } +}; + +template +requires (!has_tls_layer) +class stream_context { + mqtt_context _mqtt_context; +public: + stream_context(std::monostate) {} + + mqtt_context& mqtt_context() { + return _mqtt_context; + } + + void will(will will) { + _mqtt_context.will = std::move(will); + } + + template + auto connack_prop(Prop p) { + return _mqtt_context.ca_props[p]; + } + + void credentials( + std::string client_id, + std::string username = "", std::string password = "" + ) { + _mqtt_context.credentials = { + std::move(client_id), + std::move(username), std::move(password) + }; + } +}; + +template < + typename StreamType, + typename TlsContext = std::monostate +> +class client_service { + using stream_context_type = detail::stream_context; + using stream_type = detail::autoconnect_stream< + StreamType, stream_context_type + >; +public: + using executor_type = typename stream_type::executor_type; +private: + using tls_context_type = TlsContext; + using receive_channel = asio::experimental::concurrent_channel< + void (error_code, std::string, std::string, publish_props) + >; + + template + friend class detail::async_sender; + + template + friend class detail::assemble_op; + + template + friend class detail::ping_op; + + template + friend class detail::sentry_op; + + stream_context_type _stream_context; + stream_type _stream; + + packet_id_allocator _pid_allocator; + detail::replies _replies; + detail::async_sender _async_sender; + + std::string _read_buff; + detail::data_span _active_span; + + receive_channel _rec_channel; + + asio::cancellation_signal _cancel_ping; + asio::cancellation_signal _cancel_sentry; + +public: + + client_service( + const executor_type& ex, + const std::string& cnf, + tls_context_type tls_context = {} + ) : + _stream_context(std::move(tls_context)), + _stream(ex, _stream_context), + _async_sender(*this), + _rec_channel(ex, 128) + {} + + executor_type get_executor() const noexcept { + return _stream.get_executor(); + } + + decltype(auto) tls_context() + requires (!std::is_same_v) { + return _stream_context.tls_context(); + } + + void will(will will) { + if (!is_open()) + _stream_context.will(std::move(will)); + } + + void credentials( + std::string client_id, + std::string username = "", std::string password = "" + ) { + if (!is_open()) + _stream_context.credentials( + std::move(client_id), + std::move(username), std::move(password) + ); + } + + void brokers(std::string hosts, uint16_t default_port) { + if (!is_open()) + _stream.brokers(std::move(hosts), default_port); + } + + template + auto connack_prop(Prop p) { + return _stream_context.connack_prop(p); + } + + void open_stream() { + _stream.open(); + } + + bool is_open() const { + return _stream.is_open(); + } + + void close_stream() { + _stream.close(); + } + + void cancel() { + _cancel_ping.emit(asio::cancellation_type::terminal); + _cancel_sentry.emit(asio::cancellation_type::terminal); + + _replies.cancel_unanswered(); + _async_sender.cancel(); + _stream.close(); + } + + uint16_t allocate_pid() { + return _pid_allocator.allocate(); + } + + void free_pid(uint16_t pid, bool was_throttled = false) { + _pid_allocator.free(pid); + if (was_throttled) + _async_sender.throttled_op_done(); + } + + serial_num_t next_serial_num() { + return _async_sender.next_serial_num(); + } + + template + decltype(auto) async_send( + const BufferType& buffer, + serial_num_t serial_num, unsigned flags, + CompletionToken&& token + ) { + return _async_sender.async_send( + buffer, serial_num, flags, std::forward(token) + ); + } + + template + decltype(auto) async_assemble(duration wait_for, CompletionToken&& token) { + auto initiation = [this] (auto handler, duration wait_for) mutable { + detail::assemble_op { + *this, std::move(handler), + _read_buff, _active_span + }.perform(wait_for, asio::transfer_at_least(0)); + }; + + using signature = void ( + error_code, uint16_t, uint8_t, byte_citer, byte_citer + ); + return asio::async_initiate ( + std::move(initiation), token, wait_for + ); + } + + template + decltype(auto) async_wait_reply( + control_code_e code, uint16_t packet_id, CompletionToken&& token + ) { + return _replies.async_wait_reply( + code, packet_id, std::forward(token) + ); + } + + bool channel_store(decoders::publish_message message) { + auto& [topic, packet_id, flags, props, payload] = message; + return _rec_channel.try_send( + error_code {}, std::move(topic), + std::move(payload), std::move(props) + ); + } + + template + decltype(auto) async_channel_receive(CompletionToken&& token) { + // sig = void (error_code, std::string, std::string, publish_props) + return _rec_channel.async_receive( + std::forward(token) + ); + } + +}; + + +} // namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_CLIENT_SERVICE_HPP diff --git a/include/async_mqtt5/impl/connect_op.hpp b/include/async_mqtt5/impl/connect_op.hpp new file mode 100644 index 0000000..01d38e4 --- /dev/null +++ b/include/async_mqtt5/impl/connect_op.hpp @@ -0,0 +1,291 @@ +#ifndef ASYNC_MQTT5_CONNECT_OP_HPP +#define ASYNC_MQTT5_CONNECT_OP_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include +#include +#include + +namespace async_mqtt5::detail { + +template < + typename Stream, typename Handler +> +class connect_op { + struct on_connect {}; + struct on_tls_handshake {}; + struct on_ws_handshake {}; + struct on_send_connect {}; + struct on_fixed_header {}; + struct on_read_connack {}; + + Stream& _stream; + mqtt_context& _ctx; + std::decay_t _handler; + std::unique_ptr _buffer_ptr; + + using endpoint = asio::ip::tcp::endpoint; + using epoints = asio::ip::tcp::resolver::results_type; + +public: + connect_op( + Stream& stream, Handler&& handler, mqtt_context& ctx + ) : + _stream(stream), _ctx(ctx), + _handler(std::move(handler)) + {} + + connect_op(connect_op&&) noexcept = default; + connect_op(const connect_op&) = delete; + + using executor_type = typename Stream::executor_type; + executor_type get_executor() const noexcept { + return _stream.get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + using cancellation_slot_type = + asio::associated_cancellation_slot_t; + cancellation_slot_type get_cancellation_slot() const noexcept { + return asio::get_associated_cancellation_slot(_handler); + } + + void perform( + const connect_op::epoints& eps, authority_path ap + ) { + lowest_layer(_stream).async_connect( + *std::begin(eps), + asio::append( + asio::prepend(std::move(*this), on_connect {}), + *std::begin(eps), std::move(ap) + ) + ); + } + + void operator()( + on_connect, error_code ec, connect_op::endpoint ep, authority_path ap + ) { + if (ec) + return complete(ec); + + do_tls_handshake(std::move(ep), std::move(ap)); + } + + void do_tls_handshake(connect_op::endpoint ep, authority_path ap) { + if constexpr (has_tls_handshake) { + _stream.async_handshake( + tls_handshake_type::client, + asio::append( + asio::prepend(std::move(*this), on_tls_handshake {}), + std::move(ep), std::move(ap) + ) + ); + } + else if constexpr (has_tls_handshake::type>) { + _stream.next_layer().async_handshake( + tls_handshake_type::type>::client, + asio::append( + asio::prepend(std::move(*this), on_tls_handshake {}), + std::move(ep), std::move(ap) + ) + ); + } + else + do_ws_handshake(std::move(ep), std::move(ap)); + } + + void operator()( + on_tls_handshake, error_code ec, + connect_op::endpoint ep, authority_path ap + ) { + if (ec) + return complete(ec); + + do_ws_handshake(std::move(ep), std::move(ap)); + } + + void do_ws_handshake(connect_op::endpoint ep, authority_path ap) { + if constexpr (has_ws_handshake) { + using namespace boost::beast; + + // We'll need to turn off read timeouts on the underlying stream + // because the websocket stream has its own timeout system. + + // Set suggested timeout settings for the websocket + _stream.set_option( + websocket::stream_base::timeout::suggested(role_type::client) + ); + + _stream.binary(true); + + // Set a decorator to change the User-Agent of the handshake + _stream.set_option(websocket::stream_base::decorator( + [](websocket::request_type& req) { + req.set(http::field::sec_websocket_protocol, "mqtt"); + req.set(http::field::user_agent, "boost.mqtt"); + }) + ); + + _stream.async_handshake( + ap.host + ':' + ap.port, ap.path, + asio::prepend(std::move(*this), on_ws_handshake {}) + ); + } + else + send_connect(); + } + + void operator()(on_ws_handshake, error_code ec) { + if (ec) + return complete(ec); + + send_connect(); + } + + void send_connect() { + auto packet = control_packet::of( + no_pid, get_allocator(), + encoders::encode_connect, + _ctx.credentials.client_id, + _ctx.credentials.username, _ctx.credentials.password, + 10u, false, _ctx.co_props, _ctx.will + ); + + const auto& wire_data = packet.wire_data(); + + async_mqtt5::detail::async_write( + _stream, asio::buffer(wire_data), + asio::consign( + asio::prepend(std::move(*this), on_send_connect{}), + std::move(packet) + ) + ); + } + + void operator()(on_send_connect, error_code ec, size_t) { + if (ec) + return complete(ec); + + constexpr size_t min_connack_sz = 5; + _buffer_ptr = std::make_unique(min_connack_sz, 0); + + auto buff = asio::buffer(_buffer_ptr->data(), min_connack_sz); + asio::async_read( + _stream, buff, + asio::prepend(std::move(*this), on_fixed_header {}) + ); + } + + void operator()( + on_fixed_header, error_code ec, size_t num_read + ) { + if (ec) + return complete(ec); + + auto control_byte = (*_buffer_ptr)[0]; + if (control_byte != 0b00100000) + return complete(asio::error::try_again); + + auto varlen_ptr = _buffer_ptr->cbegin() + 1; + auto varlen = decoders::type_parse( + varlen_ptr, _buffer_ptr->cend(), decoders::basic::varint_ + ); + if (!varlen) + complete(asio::error::try_again); + + auto varlen_sz = std::distance(_buffer_ptr->cbegin() + 1, varlen_ptr); + auto remain_len = *varlen - + std::distance(varlen_ptr, _buffer_ptr->cbegin() + num_read); + + _buffer_ptr->resize(_buffer_ptr->size() + remain_len); + + auto buff = asio::buffer(_buffer_ptr->data() + num_read, remain_len); + auto first = _buffer_ptr->cbegin() + varlen_sz + 1; + auto last = first + *varlen; + + asio::async_read( + _stream, buff, + asio::prepend( + asio::append( + std::move(*this), uint8_t(control_byte), first, last + ), on_read_connack {} + ) + ); + } + + void operator()( + on_read_connack, error_code ec, size_t, uint8_t control_code, + byte_citer first, byte_citer last + ) { + if (ec) + return complete(ec); + + auto packet_length = std::distance(first, last); + auto rv = decoders::decode_connack(packet_length, first); + const auto& [session_present, reason_code, ca_props] = *rv; + + _ctx.ca_props = ca_props; + + // TODO: session_present logic + // Unexpected result handling: + // - If we don't have a Session State, and we get session_present = true, + // we must close the network connection (and restart with a clean start) + // - If we have a Session State, and we get session_present = false, + // we must discard our Session State + + auto rc = to_reason_code(reason_code); + if (!rc.has_value()) // reason code not allowed in CONNACK + return complete(client::error::malformed_packet); + + complete(to_asio_error(*rc)); + } + +private: + void complete(error_code ec) { + get_cancellation_slot().clear(); + + asio::dispatch( + get_executor(), + asio::prepend(std::move(_handler), ec) + ); + } + + static error_code to_asio_error(reason_code rc) { + using namespace boost::asio::error; + using namespace reason_codes; + + if (rc == success) + return {}; + + if (rc == unspecified_error || rc == server_unavailable || + rc == server_busy || rc == connection_rate_exceeded) + return connection_refused; + + return access_denied; + } +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_CONNECT_OP_HPP diff --git a/include/async_mqtt5/impl/disconnect_op.hpp b/include/async_mqtt5/impl/disconnect_op.hpp new file mode 100644 index 0000000..cc3391e --- /dev/null +++ b/include/async_mqtt5/impl/disconnect_op.hpp @@ -0,0 +1,140 @@ +#ifndef ASYNC_MQTT5_DISCONNECT_OP_HPP +#define ASYNC_MQTT5_DISCONNECT_OP_HPP + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template < + typename ClientService, + typename DisconnectContext, + typename Handler +> +class disconnect_op { + using client_service = ClientService; + + struct on_disconnect {}; + + std::shared_ptr _svc_ptr; + DisconnectContext _context; + std::decay_t _handler; + +public: + disconnect_op( + const std::shared_ptr& svc_ptr, + DisconnectContext&& context, Handler&& handler + ) : + _svc_ptr(svc_ptr), + _context(std::move(context)), + _handler(std::move(handler)) + {} + + disconnect_op(disconnect_op&&) noexcept = default; + disconnect_op(const disconnect_op&) = delete; + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc_ptr->get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + void perform() { + auto disconnect = control_packet::of( + no_pid, get_allocator(), + encoders::encode_disconnect, + static_cast(_context.reason_code), _context.props + ); + + send_disconnect(std::move(disconnect)); + } + + void send_disconnect(control_packet disconnect) { + const auto& wire_data = disconnect.wire_data(); + + _svc_ptr->async_send( + wire_data, + no_serial, send_flag::terminal, + asio::consign( + asio::prepend(std::move(*this), on_disconnect {}), + std::move(disconnect) + ) + ); + } + + void operator()(on_disconnect, error_code ec) { + // The connection must be closed even + // if we failed to send the DISCONNECT packet + // with Reason Code of 0x80 or greater. + // TODO: what about rc < 0x80? + + if ( + ec == asio::error::operation_aborted || + ec == asio::error::no_recovery + ) + return complete(ec); + + if (_context.terminal) { + _svc_ptr->cancel(); + return complete(error_code {}); + } + + if (ec == asio::error::try_again) + return complete(ec); + + _svc_ptr->close_stream(); + _svc_ptr->open_stream(); + + complete(error_code {}); + } + +private: + void complete(error_code ec) { + asio::dispatch( + get_executor(), + asio::prepend(std::move(_handler), ec) + ); + } +}; + +template +decltype(auto) async_disconnect( + disconnect_rc_e reason_code, const disconnect_props& props, + bool terminal, const std::shared_ptr& svc_ptr, + CompletionToken&& token +) { + using Signature = void (error_code); + + auto initiate = []( + auto handler, detail::disconnect_context ctx, bool terminal, + const std::shared_ptr& svc_ptr + ) { + detail::disconnect_op { + svc_ptr, std::move(ctx), std::move(handler) + }.perform(); + }; + + return asio::async_initiate( + std::move(initiate), token, + detail::disconnect_context { reason_code, props, terminal }, + terminal, svc_ptr + ); +} + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_DISCONNECT_HPP diff --git a/include/async_mqtt5/impl/endpoints.hpp b/include/async_mqtt5/impl/endpoints.hpp new file mode 100644 index 0000000..4b4b1d8 --- /dev/null +++ b/include/async_mqtt5/impl/endpoints.hpp @@ -0,0 +1,215 @@ +#ifndef ASYNC_MQTT5_ENDPOINTS_HPP +#define ASYNC_MQTT5_ENDPOINTS_HPP + +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +using epoints = asio::ip::tcp::resolver::results_type; + +template +class resolve_op { + struct on_resolve {}; + + Owner& _owner; + std::decay_t _handler; + +public: + resolve_op( + Owner& owner, Handler&& handler) : + _owner(owner), + _handler(std::move(handler)) + {} + + resolve_op(resolve_op&&) noexcept = default; + resolve_op(const resolve_op&) = delete; + + using executor_type = typename Owner::executor_type; + executor_type get_executor() const noexcept { + return _owner.get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + using cancellation_slot_type = + asio::associated_cancellation_slot_t; + cancellation_slot_type get_cancellation_slot() const noexcept { + return asio::get_associated_cancellation_slot(_handler); + } + + void perform() { + namespace asioex = boost::asio::experimental; + + if (_owner._servers.empty()) + return complete_post(asio::error::host_not_found, {}, {}); + + _owner._current_host++; + + if (_owner._current_host + 1 > _owner._servers.size()) { + _owner._current_host = -1; + return complete_post(asio::error::try_again, {}, {}); + } + + authority_path ap = _owner._servers[_owner._current_host]; + + _owner._connect_timer.expires_from_now(std::chrono::seconds(5)); + + auto timed_resolve = asioex::make_parallel_group( + _owner._resolver.async_resolve(ap.host, ap.port, asio::deferred), + _owner._connect_timer.async_wait(asio::deferred) + ); + + timed_resolve.async_wait( + asioex::wait_for_one(), + asio::append( + asio::prepend(std::move(*this), on_resolve {}), + std::move(ap) + ) + ); + } + + void operator()( + on_resolve, auto ord, + error_code resolve_ec, epoints epts, + error_code timer_ec, authority_path ap + ) { + if ( + ord[0] == 0 && resolve_ec == asio::error::operation_aborted || + ord[0] == 1 && timer_ec == asio::error::operation_aborted + ) + return complete(asio::error::operation_aborted, {}, {}); + + if (!resolve_ec) + return complete(error_code {}, std::move(epts), std::move(ap)); + + perform(); + } + +private: + void complete(error_code ec, epoints eps, authority_path ap) { + get_cancellation_slot().clear(); + + asio::dispatch( + get_executor(), + asio::prepend( + std::move(_handler), ec, + std::move(eps), std::move(ap) + ) + ); + } + + void complete_post(error_code ec, epoints eps, authority_path ap) { + get_cancellation_slot().clear(); + + asio::post( + get_executor(), + asio::prepend( + std::move(_handler), ec, + std::move(eps), std::move(ap) + ) + ); + + } +}; + + +class endpoints { + asio::ip::tcp::resolver _resolver; + asio::steady_timer& _connect_timer; + + std::vector _servers; + + int _current_host { -1 }; + + template + friend class resolve_op; + + template + static constexpr auto to_(T& arg) { + return [&](auto& ctx) { arg = boost::spirit::x3::_attr(ctx); }; + } + + template + static constexpr auto as_(Parser&& p){ + return boost::spirit::x3::rule{} = std::forward(p); + } + +public: + template + endpoints(Executor ex, asio::steady_timer& timer) + : _resolver(ex), _connect_timer(timer) + {} + + using executor_type = asio::ip::tcp::resolver::executor_type; + // NOTE: asio::ip::basic_resolver returns executor by value + executor_type get_executor() { + return _resolver.get_executor(); + } + + template + decltype(auto) async_next_endpoint(CompletionToken&& token) { + auto initiation = [this](auto handler) { + resolve_op { *this, std::move(handler) }.perform(); + }; + + return asio::async_initiate< + CompletionToken, + void (error_code, epoints, authority_path) + >( + std::move(initiation), token + ); + } + + void brokers(std::string hosts, uint16_t default_port) { + namespace x3 = boost::spirit::x3; + + _servers.clear(); + + std::string host, port, path; + + // loosely based on RFC 3986 + auto unreserved_ = x3::char_("-a-zA-Z_0-9._~"); + auto digit_ = x3::char_("0-9"); + auto separator_ = x3::char_(','); + + auto host_ = as_(+unreserved_)[to_(host)]; + auto port_ = as_(':' >> +digit_)[to_(port)]; + auto path_ = as_(x3::char_('/') >> *unreserved_)[to_(path)]; + auto uri_ = *x3::omit[x3::space] >> (host_ >> *port_ >> *path_) >> + (*x3::omit[x3::space] >> x3::omit[separator_ | x3::eoi]); + + for (auto b = hosts.begin(); b != hosts.end(); ) { + host.clear(); port.clear(); path.clear(); + if (phrase_parse(b, hosts.end(), uri_, x3::eps(false))) { + _servers.push_back({ + std::move(host), + port.empty() + ? std::to_string(default_port) + : std::move(port), + std::move(path) + }); + } + else b = hosts.end(); + } + } + +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_ENDPOINTS_HPP diff --git a/include/async_mqtt5/impl/internal/alloc/memory.h b/include/async_mqtt5/impl/internal/alloc/memory.h new file mode 100644 index 0000000..bb1505c --- /dev/null +++ b/include/async_mqtt5/impl/internal/alloc/memory.h @@ -0,0 +1,42 @@ +#ifndef ASYNC_MQTT5_MEMORY_H +#define ASYNC_MQTT5_MEMORY_H + +#include +#include + +#include "memory_resource.h" + +namespace pma { + +template +class alloc : public polymorphic_allocator { + using base = polymorphic_allocator; + +public: + alloc(pma::memory_resource* r) noexcept : base(r) {} + + template + alloc(const alloc& other) noexcept : base(other.resource()) {} + + using value_type = typename base::value_type; + using pointer = typename base::value_type*; + using const_pointer = const typename base::value_type*; + using reference = typename base::value_type&; + using const_reference = const typename base::value_type&; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + // https://stackoverflow.com/questions/27471053/example-usage-of-propagate-on-container-move-assignment + + using propagate_on_container_copy_assignment = std::false_type; + using propagate_on_container_move_assignment = std::false_type; + using propagate_on_container_swap = std::false_type; + + alloc select_on_container_copy_construction() const noexcept { + return alloc(base::resource()); + } +}; + +} // end namespace pma + +#endif // !ASYNC_MQTT5_MEMORY_H diff --git a/include/async_mqtt5/impl/internal/alloc/memory_resource.h b/include/async_mqtt5/impl/internal/alloc/memory_resource.h new file mode 100644 index 0000000..2d9e8e0 --- /dev/null +++ b/include/async_mqtt5/impl/internal/alloc/memory_resource.h @@ -0,0 +1,515 @@ +#ifndef ASYNC_MQTT5_MEMORY_RESOURCE_H +#define ASYNC_MQTT5_MEMORY_RESOURCE_H + +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pma { + +struct erased_type { }; + +template struct __tuple_indices {}; + +template +struct __integer_sequence { + template < + template class _ToIndexSeq, + class _ToIndexType + > + using __convert = _ToIndexSeq<_ToIndexType, _Values...>; + + template + using __to_tuple_indices = __tuple_indices<(_Values + _Sp)...>; +}; + +template +using __make_indices_imp = + typename __make_integer_seq<__integer_sequence, size_t, _Ep - _Sp>::template + __to_tuple_indices<_Sp>; + +template +struct __make_tuple_indices { + static_assert(_Sp <= _Ep, "__make_tuple_indices input error"); + using type = __make_indices_imp<_Ep, _Sp>; +}; + + + +struct allocator_arg_t { + explicit allocator_arg_t() = default; +}; + +template +struct __has_allocator_type +{ +private: + struct __two {char __lx; char __lxx;}; + template static __two __test(...); + template static char __test(typename _Up::allocator_type* = 0); +public: + static const bool value = sizeof(__test<_Tp>(0)) == 1; +}; + +template ::value> +struct __uses_allocator : + public std::integral_constant< + bool, + std::is_convertible<_Alloc, typename _Tp::allocator_type>::value + > +{}; + +template +struct __uses_allocator<_Tp, _Alloc, false> : public std::false_type {}; + +template +struct uses_allocator : public __uses_allocator<_Tp, _Alloc> {}; + +template +inline constexpr size_t uses_allocator_v = uses_allocator<_Tp, _Alloc>::value; + +template < + class _Tp, class _Alloc, + bool = std::uses_allocator<_Tp, _Alloc>::value, + bool = __has_allocator_type<_Tp>::value +> +struct __lfts_uses_allocator : public std::false_type {}; + +template +struct __lfts_uses_allocator<_Tp, _Alloc, false, false> : + public std::false_type {}; + +template +struct __lfts_uses_allocator<_Tp, _Alloc, true, HasAlloc> : + public std::true_type {}; + +template +struct __lfts_uses_allocator<_Tp, _Alloc, false, true> : + public std::integral_constant< + bool, + std::is_convertible<_Alloc, typename _Tp::allocator_type>::value + || std::is_same::value + > +{}; + +template +struct __lfts_uses_alloc_ctor_imp { + static const int value = 0; +}; + +template +struct __lfts_uses_alloc_ctor_imp +{ + static const bool __ic_first = + std::is_constructible<_Tp, std::allocator_arg_t, _Alloc, _Args...>::value; + + static const bool __ic_second = + std::conditional< + __ic_first, + std::false_type, + std::is_constructible<_Tp, _Args..., _Alloc> + >::type::value; + + static_assert( + __ic_first || __ic_second, + "Request for uses allocator construction is ill-formed" + ); + + static const int value = __ic_first ? 1 : 2; +}; + +template +struct __lfts_uses_alloc_ctor : + std::integral_constant::value + , _Tp, _Alloc, _Args... + >::value + > +{}; + +template +inline void __user_alloc_construct_impl( + std::integral_constant, _Tp *__storage, + const _Allocator &__a, _Args &&... __args +) { + new (__storage) _Tp (std::forward<_Args>(__args)..., __a); +} + +template +inline void __lfts_user_alloc_construct( + _Tp * __store, const _Alloc & __a, _Args &&... __args +) { + __user_alloc_construct_impl( + typename __lfts_uses_alloc_ctor<_Tp, _Alloc, _Args...>::type(), + __store, __a, std::forward<_Args>(__args)... + ); +} + +inline size_t __aligned_allocation_size(size_t __s, size_t __a) noexcept { + return (__s + __a - 1) & ~(__a - 1); +} + +class memory_resource { + static const size_t __max_align = alignof(max_align_t); + +public: + virtual ~memory_resource() = default; + + void* allocate(size_t __bytes, size_t __align = __max_align) { + return do_allocate(__bytes, __align); + } + + void deallocate(void * __p, size_t __bytes, size_t __align = __max_align) { + do_deallocate(__p, __bytes, __align); + } + + bool is_equal(memory_resource const & __other) const noexcept { + return do_is_equal(__other); + } + +private: + virtual void* do_allocate(size_t, size_t) = 0; + virtual void do_deallocate(void*, size_t, size_t) = 0; + virtual bool do_is_equal(memory_resource const &) const noexcept = 0; +}; + +inline bool operator==( + memory_resource const & __lhs, + memory_resource const & __rhs +) noexcept { + return &__lhs == &__rhs || __lhs.is_equal(__rhs); +} + +inline +bool operator!=( + memory_resource const & __lhs, + memory_resource const & __rhs +) noexcept { + return !(__lhs == __rhs); +} + +memory_resource* new_delete_resource() noexcept; + +memory_resource* null_memory_resource() noexcept; + +memory_resource* get_default_resource() noexcept; + +// memory_resource* set_default_resource(memory_resource * __new_res) noexcept; + +template +class polymorphic_allocator { +public: + using value_type = _ValueType; + + polymorphic_allocator() noexcept : + __res_(get_default_resource()) + {} + + polymorphic_allocator(memory_resource* __r) noexcept : + __res_(__r) + {} + + polymorphic_allocator(polymorphic_allocator const &) = default; + + template + polymorphic_allocator(polymorphic_allocator<_Tp> const & __other) noexcept : + __res_(__other.resource()) + {} + + polymorphic_allocator & + operator=(polymorphic_allocator const &) = delete; + + _ValueType* allocate(size_t __n) { + if (__n > __max_size()) + throw std::bad_array_new_length(); + return static_cast<_ValueType*>( + __res_->allocate(__n * sizeof(_ValueType), alignof(_ValueType)) + ); + } + + void deallocate(_ValueType * __p, size_t __n) noexcept { + __res_->deallocate(__p, __n * sizeof(_ValueType), alignof(_ValueType)); + } + + template + void construct(_Tp* __p, _Ts &&... __args) { + __lfts_user_alloc_construct( + __p, *this, std::forward<_Ts>(__args)... + ); + } + + template + void construct( + std::pair<_T1, _T2>* __p, std::piecewise_construct_t, + std::tuple<_Args1...> __x, std::tuple<_Args2...> __y + ) { + ::new ((void*)__p) std::pair<_T1, _T2>( + std::piecewise_construct, + __transform_tuple( + typename __lfts_uses_alloc_ctor< + _T1, polymorphic_allocator&, _Args1... + >::type(), + std::move(__x), + typename __make_tuple_indices::type{} + ), + __transform_tuple( + typename __lfts_uses_alloc_ctor< + _T2, polymorphic_allocator&, _Args2... + >::type(), + std::move(__y), + typename __make_tuple_indices::type{} + ) + ); + } + + template + void construct(std::pair<_T1, _T2>* __p) { + construct(__p, std::piecewise_construct, std::tuple<>(), std::tuple<>()); + } + + template + void construct(std::pair<_T1, _T2> * __p, _Up && __u, _Vp && __v) { + construct( + __p, std::piecewise_construct, + std::forward_as_tuple(std::forward<_Up>(__u)), + std::forward_as_tuple(std::forward<_Vp>(__v)) + ); + } + + template + void construct(std::pair<_T1, _T2> * __p, std::pair<_U1, _U2> const & __pr) { + construct( + __p, std::piecewise_construct, + std::forward_as_tuple(__pr.first), + std::forward_as_tuple(__pr.second) + ); + } + + template + void construct(std::pair<_T1, _T2> * __p, std::pair<_U1, _U2> && __pr) { + construct( + __p, std::piecewise_construct, + std::forward_as_tuple(std::forward<_U1>(__pr.first)), + std::forward_as_tuple(std::forward<_U2>(__pr.second)) + ); + } + + template + void destroy(_Tp * __p) noexcept { + __p->~_Tp(); + } + + polymorphic_allocator + select_on_container_copy_construction() const noexcept { + return polymorphic_allocator(); + } + + memory_resource* resource() const noexcept { + return __res_; + } + +private: + template + std::tuple<_Args&&...> + __transform_tuple( + std::integral_constant, std::tuple<_Args...>&& __t, + __tuple_indices<_Idx...> + ) const { + return std::forward_as_tuple(std::get<_Idx>(std::move(__t))...); + } + + template + std::tuple + __transform_tuple( + std::integral_constant, std::tuple<_Args...> && __t, + __tuple_indices<_Idx...> + ) { + using _Tup = std::tuple< + allocator_arg_t const&, + polymorphic_allocator&, _Args&&... + >; + return _Tup(allocator_arg_t{}, *this, std::get<_Idx>(std::move(__t))...); + } + + template + std::tuple<_Args&&..., polymorphic_allocator&> + __transform_tuple( + std::integral_constant, std::tuple<_Args...> && __t, + __tuple_indices<_Idx...> + ) { + using _Tup = std::tuple<_Args&&..., polymorphic_allocator&>; + return _Tup(std::get<_Idx>(std::move(__t))..., *this); + } + + size_t __max_size() const noexcept { + return std::numeric_limits::max() / sizeof(value_type); + } + + memory_resource * __res_; +}; + +template +inline bool operator==( + polymorphic_allocator<_Tp> const & __lhs, + polymorphic_allocator<_Up> const & __rhs +) noexcept { + return *__lhs.resource() == *__rhs.resource(); +} + +template +inline bool operator!=( + polymorphic_allocator<_Tp> const & __lhs, + polymorphic_allocator<_Up> const & __rhs +) noexcept { + return !(__lhs == __rhs); +} + +template +class __resource_adaptor_imp : public memory_resource { + using _CTraits = std::allocator_traits<_CharAlloc>; + static_assert( + std::is_same::value && + std::is_same::value && + std::is_same::value + ); + + static const size_t _MaxAlign = alignof(max_align_t); + + using _Alloc = typename _CTraits::template rebind_alloc< + typename std::aligned_storage<_MaxAlign, _MaxAlign>::type + >; + + using _ValueType = typename _Alloc::value_type; + + _Alloc __alloc_; + +public: + using allocator_type = _CharAlloc; + + __resource_adaptor_imp() = default; + __resource_adaptor_imp(__resource_adaptor_imp const &) = default; + __resource_adaptor_imp(__resource_adaptor_imp &&) noexcept = default; + + explicit __resource_adaptor_imp(allocator_type const & __a) : + __alloc_(__a) + {} + + explicit __resource_adaptor_imp(allocator_type && __a) : + __alloc_(std::move(__a)) + {} + + __resource_adaptor_imp & + operator=(__resource_adaptor_imp const &) = default; + + allocator_type get_allocator() const { + return __alloc_; + } + +private: + void * do_allocate(size_t __bytes, size_t) override { + if (__bytes > __max_size()) + throw std::bad_array_new_length(); + size_t __s = __aligned_allocation_size(__bytes, _MaxAlign) / _MaxAlign; + return __alloc_.allocate(__s); + } + + void do_deallocate(void * __p, size_t __bytes, size_t) override { + size_t __s = __aligned_allocation_size(__bytes, _MaxAlign) / _MaxAlign; + __alloc_.deallocate((_ValueType*)__p, __s); + } + + bool do_is_equal(memory_resource const & __other) const noexcept override { + auto __p = dynamic_cast<__resource_adaptor_imp const *>(&__other); + return __p ? __alloc_ == __p->__alloc_ : false; + } + + size_t __max_size() const noexcept { + return std::numeric_limits::max() - _MaxAlign; + } +}; + +template +using resource_adaptor = __resource_adaptor_imp< + typename std::allocator_traits<_Alloc>::template rebind_alloc +>; + +class __new_delete_memory_resource_imp : public memory_resource { + void* do_allocate(size_t size, size_t /*align*/) override { + return new std::byte[size]; + } + + void do_deallocate(void *p, size_t n, size_t align) override { + delete [](std::byte*)(p); + } + + bool do_is_equal(memory_resource const & other) const noexcept override { + return &other == this; + } + +public: + ~__new_delete_memory_resource_imp() override = default; +}; + +class __null_memory_resource_imp : public memory_resource { +public: + ~__null_memory_resource_imp() override = default; + +protected: + void* do_allocate(size_t, size_t) override { + throw std::bad_alloc(); + } + void do_deallocate(void*, size_t, size_t) override {} + bool do_is_equal(memory_resource const & __other) const noexcept override { + return &__other == this; + } +}; + +inline memory_resource* new_delete_resource() noexcept { + static __new_delete_memory_resource_imp inst { }; + return &inst; +} + +/* + +// Commented out to prevent creation of polymorphic_allocator without +// explicitly provided memory_resource + +inline std::atomic& __default_memory_resource( + bool set = false, memory_resource* = nullptr +) { + static std::atomic def { + new_delete_resource() + }; + return def; +} + + +inline memory_resource * get_default_resource() noexcept { + return __default_memory_resource(); +} + +inline memory_resource * set_default_resource(memory_resource * __new_res) noexcept { + return __default_memory_resource(true, __new_res); +} + +*/ + +} // end namespace pma + +#endif // !ASYNC_MQTT5_MEMORY_RESOURCE_H diff --git a/include/async_mqtt5/impl/internal/alloc/string.h b/include/async_mqtt5/impl/internal/alloc/string.h new file mode 100644 index 0000000..058a638 --- /dev/null +++ b/include/async_mqtt5/impl/internal/alloc/string.h @@ -0,0 +1,22 @@ +#ifndef ASYNC_MQTT5_STRING_H +#define ASYNC_MQTT5_STRING_H + +#include + +#include "memory.h" + +namespace pma { + +template > +using basic_string = + std::basic_string<_CharT, _Traits, alloc<_CharT>>; + + +using string = basic_string; +using u16string = basic_string; +using u32string = basic_string; +using wstring = basic_string; + +} // namespace pma + +#endif // !ASYNC_MQTT5_STRING_H diff --git a/include/async_mqtt5/impl/internal/alloc/vector.h b/include/async_mqtt5/impl/internal/alloc/vector.h new file mode 100644 index 0000000..4011363 --- /dev/null +++ b/include/async_mqtt5/impl/internal/alloc/vector.h @@ -0,0 +1,15 @@ +#ifndef ASYNC_MQTT5_VECTOR_H +#define ASYNC_MQTT5_VECTOR_H + +#include + +#include "memory.h" + +namespace pma { + +template +using vector = std::vector<_ValueT, alloc<_ValueT>>; + +} // namespace pma + +#endif // !ASYNC_MQTT5_VECTOR_H diff --git a/include/async_mqtt5/impl/internal/codecs/base_decoders.hpp b/include/async_mqtt5/impl/internal/codecs/base_decoders.hpp new file mode 100644 index 0000000..c26ca97 --- /dev/null +++ b/include/async_mqtt5/impl/internal/codecs/base_decoders.hpp @@ -0,0 +1,408 @@ +#ifndef ASYNC_MQTT5_BASE_DECODERS_HPP +#define ASYNC_MQTT5_BASE_DECODERS_HPP + +#include +#include +#include + +#include +#include + + +namespace async_mqtt5::decoders { + +namespace x3 = boost::spirit::x3; + +template +struct convert { using type = T; }; + +template +struct convert> { + using type = std::tuple::type...>; +}; + +template +struct convert> { + using type = std::optional; +}; + +template +struct convert> { + using type = std::vector::type>; +}; + +template +constexpr auto as(Parser&& p) { + return x3::rule{} = std::forward(p); +} + + +template +auto type_parse(It& first, const It last, const Parser& p) { + using ctx_type = decltype(x3::make_context(std::declval())); + using attr_type = typename x3::traits::attribute_of::type; + + using rv_type = typename convert::type; + + std::optional rv; + rv_type value {}; + if (x3::phrase_parse(first, last, as(p), x3::eps(false), value)) + rv = std::move(value); + return rv; +} + + +template +auto type_parse(It& first, const It last, const Parser& p) { + std::optional rv; + AttributeType value {}; + if (x3::phrase_parse(first, last, as(p), x3::eps(false), value)) + rv = std::move(value); + return rv; +} + +namespace basic { + +template +constexpr auto to(T& arg) { + return [&](auto& ctx) { + using ctx_type = decltype(ctx); + using attr_type = decltype(x3::_attr(std::declval())); + if constexpr (is_boost_iterator) + arg = T { x3::_attr(ctx).begin(), x3::_attr(ctx).end() }; + else + arg = x3::_attr(ctx); + }; +} + +template +class scope_limit {}; + +template +requires (x3::traits::is_parser::value) +class scope_limit : + public x3::unary_parser> { + + using base_type = x3::unary_parser>; + LenParser _lp; +public: + using ctx_type = decltype(x3::make_context(std::declval())); + using attribute_type = typename x3::traits::attribute_of::type; + static bool const has_attribute = true; + + scope_limit(const LenParser& lp, const Subject& subject) : base_type(subject), _lp(lp) {} + + template + bool parse(It& first, const It last, const Ctx& ctx, RCtx& rctx, Attr& attr) const { + + It iter = first; + typename x3::traits::attribute_of::type len; + if (!_lp.parse(iter, last, ctx, rctx, len)) + return false; + if (iter + len > last) + return false; + + if (!base_type::subject.parse(iter, iter + len, ctx, rctx, attr)) + return false; + + first = iter; + return true; + } +}; + +template +requires (std::is_arithmetic_v) +class scope_limit : + public x3::unary_parser> { + + using base_type = x3::unary_parser>; + size_t _limit; +public: + using ctx_type = decltype(x3::make_context(std::declval())); + using attribute_type = typename x3::traits::attribute_of::type; + static bool const has_attribute = true; + + scope_limit(Size limit, const Subject& subject) : base_type(subject), _limit(limit) {} + + template + bool parse(It& first, const It last, const Ctx& ctx, RCtx& rctx, Attr& attr) const { + + It iter = first; + if (iter + _limit > last) + return false; + + if (!base_type::subject.parse(iter, iter + _limit, ctx, rctx, attr)) + return false; + + first = iter; + return true; + } +}; + +template +struct scope_limit_gen { + template + auto operator[](const Subject& p) const { + return scope_limit { _lp, x3::as_parser(p) }; + } + LenParser _lp; +}; + +template +requires (std::is_arithmetic_v) +struct scope_limit_gen { + template + auto operator[](const Subject& p) const { + return scope_limit { limit, x3::as_parser(p) }; + } + Size limit; +}; + +template +requires (x3::traits::is_parser::value) +scope_limit_gen scope_limit_(const Parser& p) { + return { p }; +} + +template +requires (std::is_arithmetic_v) +scope_limit_gen scope_limit_(Size limit) { + return { limit }; +} + + +struct verbatim_parser : x3::parser { + using attribute_type = std::string; + static bool const has_attribute = true; + + template + bool parse(It& first, const It last, const Ctx&, RCtx&, Attr& attr) const { + attr = std::string { first, last }; + first = last; + return true; + } +}; + +constexpr auto verbatim_ = verbatim_parser{}; + +struct varint_parser : x3::parser { + using attribute_type = uint32_t; + static bool const has_attribute = true; + + template + bool parse(It& first, const It last, const Ctx& ctx, RCtx& rctx, Attr& attr) const { + + It iter = first; + x3::skip_over(iter, last, ctx); + + if (iter == last) + return false; + + uint32_t result = 0; unsigned bit_shift = 0; + + for (; iter != last && bit_shift < sizeof(int32_t) * 7; ++iter) { + auto val = *iter; + if (val & 0b1000'0000u) { + result |= (val & 0b0111'1111u) << bit_shift; + bit_shift += 7; + } + else { + result |= (static_cast(val) << bit_shift); + bit_shift = 0; + break; + } + } + if (bit_shift) + return false; + + attr = result; + first = ++iter; + return true; + } +}; + +constexpr varint_parser varint_{}; + +struct len_prefix_parser : x3::parser { + using attribute_type = std::string; + static bool const has_attribute = true; + + template + bool parse(It& first, const It last, const Ctx& ctx, RCtx& rctx, Attr& attr) const { + It iter = first; + x3::skip_over(iter, last, ctx); + + typename x3::traits::attribute_of::type len; + if (x3::big_word.parse(iter, last, ctx, rctx, len)) { + if (std::distance(iter, last) < len) + return false; + } + else + return false; + + attr = std::string(iter, iter + len); + first = iter + len; + return true; + } +}; + +constexpr len_prefix_parser utf8_{}; +constexpr len_prefix_parser binary_{}; + +/* + Boost Spirit incorrectly deduces atribute type for a parser of the form + (eps(a) | parser1) >> (eps(b) | parser) + and we had to create if_ parser to remedy the issue +*/ + +template +class conditional_parser : public x3::unary_parser> { + using base_type = x3::unary_parser>; + bool _condition; +public: + using ctx_type = decltype(x3::make_context(std::declval())); + using subject_attr_type = typename x3::traits::attribute_of::type; + + using attribute_type = boost::optional; + static bool const has_attribute = true; + + conditional_parser(const Subject& s, bool condition) : base_type(s), _condition(condition) {} + + template + bool parse(It& first, const It last, const Ctx& ctx, RCtx& rctx, Attr& attr) const { + if (!_condition) + return true; + + It iter = first; + subject_attr_type sattr {}; + if (!base_type::subject.parse(iter, last, ctx, rctx, sattr)) + return false; + + attr.emplace(std::move(sattr)); + first = iter; + return true; + } +}; + +struct conditional_gen { + bool _condition; + + template + auto operator[](const Subject& p) const { + return conditional_parser { p, _condition }; + } +}; + +inline conditional_gen if_(bool condition) { + return { condition }; +} + +} // end namespace basic + + +namespace prop { + +namespace basic = async_mqtt5::decoders::basic; + +namespace detail { + +template +bool parse_to_prop(It& iter, const It last, const Ctx& ctx, RCtx& rctx, Prop& prop) { + using prop_type = decltype(prop); + + bool rv = false; + if constexpr (is_optional) { + using value_type = typename std::remove_reference_t::value_type; + if constexpr (std::is_same_v) { + uint8_t attr; + rv = x3::byte_.parse(iter, last, ctx, rctx, attr); + prop = attr; + } + if constexpr (std::is_same_v) { + int16_t attr; + rv = x3::big_word.parse(iter, last, ctx, rctx, attr); + prop = attr; + } + if constexpr (std::is_same_v) { + uint16_t attr; + rv = x3::big_word.parse(iter, last, ctx, rctx, attr); + prop = attr; + } + if constexpr (std::is_same_v) { + int32_t attr; + rv = x3::big_dword.parse(iter, last, ctx, rctx, attr); + prop = attr; + } + if constexpr (std::is_same_v) { + uint32_t attr; + rv = basic::varint_.parse(iter, last, ctx, rctx, attr); + prop = attr; + } + if constexpr (std::is_same_v) { + std::string attr; + rv = basic::utf8_.parse(iter, last, ctx, rctx, attr); + prop.emplace(std::move(attr)); + } + } + + if constexpr (async_mqtt5::is_vector) { + std::string value; + rv = basic::utf8_.parse(iter, last, ctx, rctx, value); + if (rv) prop.push_back(std::move(value)); + } + return rv; +} + +} // end namespace detail + +template +class prop_parser : public x3::parser> { +public: + using attribute_type = Props; + static bool const has_attribute = true; + + template + bool parse(It& first, const It last, const Ctx& ctx, RCtx& rctx, Attr& attr) const { + + It iter = first; + x3::skip_over(iter, last, ctx); + + if (iter == last) + return true; + + uint32_t props_length; + if (!basic::varint_.parse(iter, last, ctx, rctx, props_length)) + return false; + + const It scoped_last = iter + props_length; + // attr = Props{}; + + while (iter < scoped_last) { + uint8_t prop_id = *iter++; + bool rv = true; + It saved = iter; + + attr.apply_on(prop_id, [&rv, &iter, scoped_last, &ctx, &rctx](auto& prop) { + rv = detail::parse_to_prop(iter, scoped_last, ctx, rctx, prop); + }); + + // either rv = false or property with prop_id was not found + if (!rv || iter == saved) + break; + } + + first = iter; + return true; + } +}; + + +template +constexpr auto props_ = prop_parser{}; + +} // end namespace prop + + +} // end namespace async_mqtt5::decoders + +#endif // !ASYNC_MQTT5_BASE_DECODERS_HPP diff --git a/include/async_mqtt5/impl/internal/codecs/base_encoders.hpp b/include/async_mqtt5/impl/internal/codecs/base_encoders.hpp new file mode 100644 index 0000000..2dba2bd --- /dev/null +++ b/include/async_mqtt5/impl/internal/codecs/base_encoders.hpp @@ -0,0 +1,492 @@ +#ifndef ASYNC_MQTT5_BASE_ENCODERS_HPP +#define ASYNC_MQTT5_BASE_ENCODERS_HPP + +#include +#include +#include + +#include +#include + +namespace async_mqtt5::encoders { + +namespace basic { + +inline void to_variable_bytes(std::string& s, int32_t val) { + if (val > 0xfffffff) return; + while (val > 127) { + s.push_back(char((val & 0b01111111) | 0b10000000)); + val >>= 7; + } + s.push_back(val & 0b01111111); +} + +inline size_t variable_length(int32_t val) { + if (val > 0xfffffff) return 0; + size_t rv = 1; + for (; val > 127; ++rv) val >>= 7; + return rv; +} + +struct encoder {}; + +template +class flag_def : public encoder { + template + using least_type = std::conditional_t< + num_bits <= 8, uint8_t, + std::conditional_t< + num_bits <= 16, uint16_t, + std::conditional_t< + num_bits <= 32, uint32_t, + std::conditional_t + > + > + >; + + template + friend class flag_def; + + repr _val { 0 }; + +public: + flag_def(repr val) : _val(val) {} + flag_def() = default; + + template + requires (is_optional) + auto operator()(T&& value, projection proj = {}) const { + if constexpr (std::is_same_v) { + repr val = value.has_value(); + return flag_def { val }; + } + else { + repr val = value.has_value() ? static_cast(std::invoke(proj, *value)) : 0; + return flag_def { val }; + } + } + + template + requires (!is_optional) + auto operator()(T&& value, projection proj = {}) const { + auto val = static_cast(std::invoke(proj, value)); + return flag_def { val }; + } + + uint16_t byte_size() const { return sizeof(repr); } + + template + auto operator|(const flag_def& rhs) const { + using res_repr = least_type; + auto val = static_cast((_val << rhs_bits) | rhs._val); + return flag_def { val }; + } + + std::string& encode(std::string& s) const { + using namespace boost::endian; + size_t sz = s.size(); s.resize(sz + sizeof(repr)); + auto p = reinterpret_cast(s.data() + sz); + endian_store(p, _val); + return s; + } +}; + +template +constexpr auto flag = flag_def{}; + + +template +class int_val : public encoder { + T _val; +public: + int_val(T val) : _val(val) {} + + uint16_t byte_size() const { + if constexpr (is_optional) { + if (_val) return uint16_t(val_length(*_val)); + return uint16_t(0); + } + else + return uint16_t(val_length(_val)); + } + + std::string& encode(std::string& s) const { + if constexpr (is_optional) { + if (_val) return encode_val(s, *_val); + return s; + } + else + return encode_val(s, _val); + } +private: + template + static size_t val_length(U&& val) { + if constexpr (std::is_same_v) + return variable_length(int32_t(val)); + else + return sizeof(Repr); + } + + template + static std::string& encode_val(std::string& s, U&& val) { + using namespace boost::endian; + if constexpr (std::is_same_v) { + to_variable_bytes(s, int32_t(val)); + return s; + } + else { + size_t sz = s.size(); s.resize(sz + sizeof(Repr)); + auto p = reinterpret_cast(s.data() + sz); + endian_store(p, val); + return s; + } + } +}; + +template +class int_def { +public: + template + auto operator()(T&& val) const { + return int_val { std::forward(val) }; + } + + template + auto operator()(T&& val, projection proj) const { + if constexpr (is_optional) { + using rv_type = std::invoke_result_t< + projection, typename std::remove_cvref_t::value_type + >; + if (val.has_value()) + return (*this)(std::invoke(proj, *val)); + return int_val { rv_type {} }; + } + else { + using rv_type = std::invoke_result_t; + return int_val { std::invoke(proj, val) }; + } + } +}; + +constexpr auto byte_ = int_def{}; +constexpr auto int16_ = int_def{}; +constexpr auto int32_ = int_def{}; +constexpr auto varlen_ = int_def{}; + + +template +class array_val : public encoder { + T _val; + bool _with_length; +public: + array_val(T val, bool with_length) : _val(val), _with_length(with_length) { + static_assert(std::is_reference_v || std::is_same_v); + } + + uint16_t byte_size() const { + if constexpr (is_optional) + return uint16_t(_val ? _with_length * 2 + val_length(*_val) : 0); + else + return uint16_t(_with_length * 2 + val_length(_val)); + } + + std::string& encode(std::string& s) const { + if constexpr (is_optional) { + if (_val) return encode_val(s, *_val); + return s; + } + else + return encode_val(s, _val); + } + +private: + template + static size_t val_length(U&& val) { + if constexpr (std::same_as, const char*>) + return std::strlen(val); + if constexpr (requires { val.size(); }) + return val.size(); + else // fallback to type const char (&)[N] (substract 1 for trailing 0) + return sizeof(val) - 1; + } + + template + std::string& encode_val(std::string& s, U&& u) const { + using namespace boost::endian; + int16_t byte_len = int16_t(val_length(std::forward(u))); + if (byte_len == 0 && !_with_length) return s; + if (_with_length) { + size_t sz = s.size(); s.resize(sz + 2); + auto p = reinterpret_cast(s.data() + sz); + endian_store(p, byte_len); + } + s.append(std::begin(u), std::begin(u) + byte_len); + return s; + } +}; + +template +class array_def { +public: + template + auto operator()(T&& val) const { + return array_val { std::forward(val), with_length }; + } + + template + auto operator()(T&& val, projection proj) const { + if constexpr (is_optional) { + using rv_type = std::invoke_result_t< + projection, typename std::remove_cvref_t::value_type + >; + if (val.has_value()) + return (*this)(std::invoke(proj, *val)); + return array_val { rv_type {}, false }; + } + else { + const auto& av = std::invoke(proj, val); + return array_val { av, true }; + } + } +}; + +using utf8_def = array_def; + +constexpr auto utf8_ = utf8_def{}; +constexpr auto binary_ = array_def{}; // for now +constexpr auto verbatim_ = array_def{}; + + +template +class composed_val : public encoder { + T _lhs; U _rhs; +public: + composed_val(T lhs, U rhs) : + _lhs(std::forward(lhs)), _rhs(std::forward(rhs)) {} + + uint16_t byte_size() const { + return uint16_t(_lhs.byte_size() + _rhs.byte_size()); + } + + std::string& encode(std::string& s) const { + _lhs.encode(s); + return _rhs.encode(s); + } +}; + +template +requires (std::derived_from, encoder> && std::derived_from, encoder>) +inline auto operator&(T&& t, U&& u) { + return composed_val(std::forward(t), std::forward(u)); +} + +template +requires (std::derived_from, encoder>) +std::string& operator<<(std::string& s, T&& t) { + return t.encode(s); +} + +} // end namespace basic + +namespace detail { +template +constexpr bool match_v = std::is_same_v::key>; + +template >> +struct type_index; + +template typename Tuple, typename... Args, std::size_t... Is> +struct type_index, std::index_sequence> + : std::integral_constant>)+... + 0)> { + static_assert(1 == (match_v> + ... + 0), "T doesn't appear once in tuple"); +}; +} // end namespace detail + +namespace prop { + + +namespace pp = async_mqtt5::prop; + +template +struct prop_encoder_type { using key = decltype(p); using value = T; }; + +using encoder_types = std::tuple< + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type, + prop_encoder_type, + prop_encoder_type, + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type, + prop_encoder_type>, + prop_encoder_type, + prop_encoder_type, + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type, + prop_encoder_type, + prop_encoder_type, + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type, + prop_encoder_type>, + prop_encoder_type>, + prop_encoder_type> +>; + +template +constexpr auto encoder_for_prop = typename std::tuple_element_t< + detail::type_index::value, encoder_types +>::value {}; + + +template +class prop_val; + +template +requires (!is_vector && is_optional) +class prop_val : public basic::encoder { + // T is always std::optional + using opt_type = typename std::remove_cvref_t::value_type; + // allows T to be reference type to std::optional + static inline std::optional nulltype; + T _val; +public: + prop_val(T val) : _val(val) { + static_assert(std::is_reference_v); + } + prop_val() : _val(nulltype) {} + + size_t byte_size() const { + if (!_val) return 0; + auto sval = encoder_for_prop

(_val); + return 1 + sval.byte_size(); + } + + std::string& encode(std::string& s) const { + if (!_val) + return s; + s.push_back(p()); + auto sval = encoder_for_prop

(_val); + return sval.encode(s); + } +}; + +template +requires (is_vector) +class prop_val : public basic::encoder { + // allows T to be reference type to std::vector + static inline std::remove_cvref_t nulltype; + T _val; +public: + prop_val(T val) : _val(val) { + static_assert(std::is_reference_v); + } + + prop_val() : _val(nulltype) { } + + size_t byte_size() const { + if (_val.empty()) return 0; + size_t total_size = 0; + for (const auto& pr: _val) { + auto sval = encoder_for_prop

(pr); + size_t prop_size = sval.byte_size(); + if (prop_size) total_size += 1 + prop_size; + } + return total_size; + } + + std::string& encode(std::string& s) const { + if (_val.empty()) + return s; + + for (const auto& pr: _val) { + auto sval = encoder_for_prop

(pr); + s.push_back(p()); + sval.encode(s); + } + return s; + } +}; + + +template +class props_val : public basic::encoder { + static inline std::decay_t nulltype; + + template + static auto to_prop_val(const T& val) { + return prop_val(val); + } + + template + static auto to_prop_vals(const pp::properties& props) { + return std::make_tuple(to_prop_val(props[Ps])...); + } + + template + auto apply_each(Func&& func) const { + return std::apply([&func](const auto&... props) { + return (std::invoke(func, props), ...); + }, _prop_vals); + } + + decltype(to_prop_vals(std::declval())) _prop_vals; + bool _may_omit; +public: + props_val(Props val, bool may_omit) : _prop_vals(to_prop_vals(val)), _may_omit(may_omit) { + static_assert(std::is_reference_v); + } + props_val(bool may_omit) : _prop_vals(to_prop_vals(nulltype)), _may_omit(may_omit) { } + + size_t byte_size() const { + size_t psize = props_size(); + if (_may_omit && psize == 0) return 0; + return psize + basic::varlen_(psize).byte_size(); + } + + std::string& encode(std::string& s) const { + size_t psize = props_size(); + if (_may_omit && psize == 0) return s; + basic::varlen_(psize).encode(s); + apply_each([&s](const auto& pv) { return pv.encode(s); }); + return s; + } +private: + size_t props_size() const { + size_t retval = 0; + apply_each([&retval](const auto& pv) { return retval += pv.byte_size(); }); + return retval; + } +}; + +template +class props_def { +public: + template + auto operator()(T&& prop_container) const { + if constexpr (is_optional) { + if (prop_container.has_value()) + return (*this)(*prop_container); + return props_val::value_type&>(true); + } + else { + return props_val { prop_container, may_omit }; + } + } +}; + +constexpr auto props_ = props_def{}; +constexpr auto props_may_omit_ = props_def{}; + +} // end namespace prop + +} // end namespace async_mqtt5::encoders + +#endif // !ASYNC_MQTT5_BASE_ENCODERS_HPP diff --git a/include/async_mqtt5/impl/internal/codecs/message_decoders.hpp b/include/async_mqtt5/impl/internal/codecs/message_decoders.hpp new file mode 100644 index 0000000..3062e52 --- /dev/null +++ b/include/async_mqtt5/impl/internal/codecs/message_decoders.hpp @@ -0,0 +1,284 @@ +#ifndef ASYNC_MQTT5_MESSAGE_DECODERS_HPP +#define ASYNC_MQTT5_MESSAGE_DECODERS_HPP + +#include +#include + +#include +#include +#include + +namespace async_mqtt5::decoders { + +using byte_citer = detail::byte_citer; + +using fixed_header = std::tuple< + uint8_t, // control byte + uint32_t // remaining_length +>; + +inline std::optional decode_fixed_header( + byte_citer& it, const byte_citer last +) { + auto fixed_header_ = x3::byte_ >> basic::varint_; + return type_parse(it, last, fixed_header_); +} + +using packet_id = uint16_t; + +inline std::optional decode_packet_id( + byte_citer& it +) { + auto packet_id_ = x3::big_word; + return type_parse(it, it + sizeof(uint16_t), packet_id_); +} + +using connect_message = std::tuple< + std::string, // client_id, + std::optional, // user_name, + std::optional, // password, + uint16_t, // keep_alive, + bool, // clean_start, + connect_props, // props, + std::optional // will +>; + +inline std::optional decode_connect( + uint32_t remain_length, byte_citer& it +) { + auto var_header_ = + basic::utf8_ >> // MQTT + x3::byte_ >> // (num 5) + x3::byte_ >> // conn_flags_ + x3::big_word >> // keep_alive + prop::props_; + + auto vh = type_parse(it, it + remain_length, var_header_); + if (!vh) + return std::optional{}; + + auto& [mqtt_str, version, flags, keep_alive, cprops] = *vh; + + if (mqtt_str != "MQTT" || version != 5) + return std::optional{}; + + bool has_will = (flags & 0b00000100); + bool has_uname = (flags & 0b10000000); + bool has_pwd = (flags & 0b01000000); + + auto payload_ = + basic::utf8_ >> // client_id + basic::if_(has_will)[prop::props_] >> + basic::if_(has_will)[basic::utf8_] >> // will topic + basic::if_(has_will)[basic::binary_] >> // will message + basic::if_(has_uname)[basic::utf8_] >> // username + basic::if_(has_pwd)[basic::utf8_]; // password + + auto pload = type_parse(it, it + remain_length, payload_); + if (!pload) + return std::optional{}; + + std::optional w; + + if (has_will) + w.emplace( + std::move(*std::get<2>(*pload)), // will_topic + std::move(*std::get<3>(*pload)), // will_message + qos_e((flags & 0b00011000) >> 3), + retain_e((flags & 0b00100000) >> 5), + std::move(*std::get<1>(*pload)) // will props + ); + + connect_message retval = { + std::move(std::get<0>(*pload)), // client_id + std::move(std::get<4>(*pload)), // user_name + std::move(std::get<5>(*pload)), // password + keep_alive, + flags & 0b00000010, // clean_start + std::move(cprops), // connect_props + std::move(w) // will + }; + + return std::optional { std::move(retval) }; +} + +using connack_message = std::tuple< + uint8_t, // session_present + uint8_t, // connect reason code + connack_props // props +>; + +inline std::optional decode_connack( + uint32_t remain_length, byte_citer& it +) { + auto connack_ = basic::scope_limit_(remain_length)[ + x3::byte_ >> x3::byte_ >> prop::props_ + ]; + return type_parse(it, it + remain_length, connack_); +} + +using publish_message = std::tuple< + std::string, // topic + std::optional, // packet_id + uint8_t, // dup_e, qos_e, retain_e + publish_props, // publish props + std::string // payload +>; + +inline std::optional decode_publish( + uint8_t control_byte, uint32_t remain_length, byte_citer& it +) { + uint8_t flags = control_byte & 0b1111; + auto qos = qos_e((flags >> 1) & 0b11); + + auto publish_ = basic::scope_limit_(remain_length)[ + basic::utf8_ >> basic::if_(qos != qos_e::at_most_once)[x3::big_word] >> x3::attr(flags) >> + prop::props_ >> basic::verbatim_ + ]; + return type_parse(it, it + remain_length, publish_); +} + +using puback_message = std::tuple< + uint8_t, // puback reason code + puback_props // props +>; + +inline std::optional decode_puback( + uint32_t remain_length, byte_citer& it +) { + auto puback_ = basic::scope_limit_(remain_length)[ + x3::byte_ >> prop::props_ + ]; + return type_parse(it, it + remain_length, puback_); +} + +using pubrec_message = std::tuple< + uint8_t, // puback reason code + pubrec_props // props +>; + +inline std::optional decode_pubrec( + uint32_t remain_length, byte_citer& it +) { + auto pubrec_ = basic::scope_limit_(remain_length)[ + x3::byte_ >> prop::props_ + ]; + return type_parse(it, it + remain_length, pubrec_); +} + +using pubrel_message = std::tuple< + uint8_t, // puback reason code + pubrel_props // props +>; + +inline std::optional decode_pubrel( + uint32_t remain_length, byte_citer& it +) { + auto pubrel_ = basic::scope_limit_(remain_length)[ + x3::byte_ >> prop::props_ + ]; + return type_parse(it, it + remain_length, pubrel_); +} + +using pubcomp_message = std::tuple< + uint8_t, // puback reason code + pubcomp_props // props +>; + +inline std::optional decode_pubcomp( + uint32_t remain_length, byte_citer& it +) { + auto pubcomp_ = basic::scope_limit_(remain_length)[ + x3::byte_ >> prop::props_ + ]; + return type_parse(it, it + remain_length, pubcomp_); +} + +using subscribe_message = std::tuple< + subscribe_props, + std::vector> // topic filter with opts +>; + +inline std::optional decode_subscribe( + uint32_t remain_length, byte_citer& it +) { + auto subscribe_ = basic::scope_limit_(remain_length)[ + prop::props_ >> +(basic::utf8_ >> x3::byte_) + ]; + return type_parse(it, it + remain_length, subscribe_); +} + +using suback_message = std::tuple< + suback_props, + std::vector // reason_codes +>; + +inline std::optional decode_suback( + uint32_t remain_length, byte_citer& it +) { + auto suback_ = basic::scope_limit_(remain_length)[ + prop::props_ >> +x3::byte_ + ]; + return type_parse(it, it + remain_length, suback_); +} + +using unsubscribe_message = std::tuple< + unsubscribe_props, + std::vector // topics +>; + +inline std::optional decode_unsubscribe( + uint32_t remain_length, byte_citer& it +) { + auto unsubscribe_ = basic::scope_limit_(remain_length)[ + prop::props_ >> +basic::utf8_ + ]; + return type_parse(it, it + remain_length, unsubscribe_); +} + +using unsuback_message = std::tuple< + unsuback_props, + std::vector // reason_codes +>; + +inline std::optional decode_unsuback( + uint32_t remain_length, byte_citer& it +) { + auto unsuback_ = basic::scope_limit_(remain_length)[ + prop::props_ >> +x3::byte_ + ]; + return type_parse(it, it + remain_length, unsuback_); +} + +using disconnect_message = std::tuple< + uint8_t, // reason_code + disconnect_props +>; + +inline std::optional decode_disconnect( + uint32_t remain_length, byte_citer& it +) { + auto disconnect_ = basic::scope_limit_(remain_length)[ + x3::byte_ >> prop::props_ + ]; + return type_parse(it, it + remain_length, disconnect_); +} + +using auth_message = std::tuple< + uint8_t, // reason_code + auth_props +>; + +inline std::optional decode_auth( + uint32_t remain_length, byte_citer& it +) { + auto auth_ = basic::scope_limit_(remain_length)[ + x3::byte_ >> prop::props_ + ]; + return type_parse(it, it + remain_length, auth_); +} + + +} // end namespace async_mqtt5::decoders + +#endif // !ASYNC_MQTT5_MESSAGE_DECODERS_HPP diff --git a/include/async_mqtt5/impl/internal/codecs/message_encoders.hpp b/include/async_mqtt5/impl/internal/codecs/message_encoders.hpp new file mode 100644 index 0000000..464b1ab --- /dev/null +++ b/include/async_mqtt5/impl/internal/codecs/message_encoders.hpp @@ -0,0 +1,422 @@ +#ifndef ASYNC_MQTT5_MESSAGE_ENCODERS_HPP +#define ASYNC_MQTT5_MESSAGE_ENCODERS_HPP + +#include +#include + +#include +#include +#include + +namespace async_mqtt5::encoders { + +template +std::string encode(const encoder& e) { + std::string s; + s.reserve(e.byte_size()); + s << e; + return s; +} + +inline std::string encode_connect( + std::string_view client_id, + std::optional user_name, + std::optional password, + uint16_t keep_alive, bool clean_start, + const connect_props& props, + const std::optional& w +) { + + auto packet_type_ = + basic::flag<4>(0b0001) | + basic::flag<4>(0); + + auto conn_flags_ = + basic::flag<1>(user_name) | + basic::flag<1>(password) | + basic::flag<1>(w, &will::retain) | + basic::flag<2>(w, &will::qos) | + basic::flag<1>(w) | + basic::flag<1>(clean_start) | + basic::flag<1>(0); + + auto var_header_ = + basic::utf8_("MQTT") & + basic::byte_(uint8_t(5)) & + conn_flags_ & + basic::int16_(keep_alive) & + prop::props_(props); + + auto payload_ = + basic::utf8_(client_id) & + prop::props_(w) & + basic::utf8_(w, &will::topic) & + basic::binary_(w, &will::message) & + basic::utf8_(user_name) & + basic::utf8_(password); + + auto message_body_ = var_header_ & payload_; + + auto fixed_header_ = + packet_type_ & + basic::varlen_(message_body_.byte_size()); + + auto connect_message_ = fixed_header_ & message_body_; + + return encode(connect_message_); +} + +inline std::string encode_connack( + bool session_present, + uint8_t reason_code, + const connack_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b0010) | + basic::flag<4>(0); + + auto var_header_ = + basic::flag<1>(session_present) & + basic::byte_(reason_code) & + prop::props_(props); + + auto fixed_header_ = + packet_type_ & + basic::varlen_(var_header_.byte_size()); + + auto connack_message_ = fixed_header_ & var_header_; + + return encode(connack_message_); +} + +inline std::string encode_publish( + uint16_t packet_id, + std::string_view topic_name, + std::string_view payload, + qos_e qos, retain_e retain, dup_e dup, + const publish_props& props +) { + + std::optional used_packet_id; + if (qos != qos_e::at_most_once) used_packet_id.emplace(packet_id); + + auto packet_type_ = + basic::flag<4>(0b0011) | + basic::flag<1>(dup) | + basic::flag<2>(qos) | + basic::flag<1>(retain); + + auto var_header_ = + basic::utf8_(topic_name) & + basic::int16_(used_packet_id) & + prop::props_(props); + + auto message_body_ = var_header_ & basic::verbatim_(payload); + + auto fixed_header_ = + packet_type_ & + basic::varlen_(message_body_.byte_size()); + + auto publish_message_ = fixed_header_ & message_body_; + + return encode(publish_message_); +} + +inline std::string encode_puback( + uint16_t packet_id, + uint8_t reason_code, + const puback_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b0100) | + basic::flag<4>(0); + + auto var_header_ = + basic::int16_(packet_id) & + basic::byte_(reason_code) & + prop::props_may_omit_(props); + + auto fixed_header_ = + packet_type_ & + basic::varlen_(var_header_.byte_size()); + + auto puback_message_ = fixed_header_ & var_header_; + + return encode(puback_message_); +} + +inline std::string encode_pubrec( + uint16_t packet_id, + uint8_t reason_code, + const pubrec_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b0101) | + basic::flag<4>(0b0000); + + auto var_header_ = + basic::int16_(packet_id) & + basic::byte_(reason_code) & + prop::props_may_omit_(props); + + auto fixed_header_ = + packet_type_ & + basic::varlen_(var_header_.byte_size()); + + auto pubrec_message_ = fixed_header_ & var_header_; + + return encode(pubrec_message_); +} + +inline std::string encode_pubrel( + uint16_t packet_id, + uint8_t reason_code, + const pubrel_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b0110) | + basic::flag<4>(0b0010); + + auto var_header_ = + basic::int16_(packet_id) & + basic::byte_(reason_code) & + prop::props_may_omit_(props); + + auto fixed_header_ = + packet_type_ & + basic::varlen_(var_header_.byte_size()); + + auto pubrel_message_ = fixed_header_ & var_header_; + + return encode(pubrel_message_); +} + +inline std::string encode_pubcomp( + uint16_t packet_id, + uint8_t reason_code, + const pubcomp_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b0111) | + basic::flag<4>(0b0000); + + auto var_header_ = + basic::int16_(packet_id) & + basic::byte_(reason_code) & + prop::props_may_omit_(props); + + auto fixed_header_ = + packet_type_ & + basic::varlen_(var_header_.byte_size()); + + auto pubcomp_message_ = fixed_header_ & var_header_; + + return encode(pubcomp_message_); +} + +inline std::string encode_subscribe( + uint16_t packet_id, + const std::vector& topics, + const subscribe_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b1000) | + basic::flag<4>(0b0010); + + size_t payload_size = 0; + for (const auto& [topic_filter, _]: topics) + payload_size += basic::utf8_(topic_filter).byte_size() + 1; + + auto var_header_ = + basic::int16_(packet_id) & + prop::props_(props); + + auto message_ = + packet_type_ & + basic::varlen_(var_header_.byte_size() + payload_size) & + var_header_; + + auto s = encode(message_); + s.reserve(s.size() + payload_size); + + for (const auto& [topic_filter, sub_opts]: topics) { + auto opts_ = + basic::flag<2>(sub_opts.retain_handling) | + basic::flag<1>(sub_opts.retain_as_published) | + basic::flag<1>(sub_opts.no_local) | + basic::flag<2>(sub_opts.max_qos); + auto filter_ = basic::utf8_(topic_filter) & opts_; + s << filter_; + } + + return s; +} + +inline std::string encode_suback( + uint16_t packet_id, + std::vector& reason_codes, + const suback_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b1001) | + basic::flag<4>(0b0000); + + auto var_header_ = + basic::int16_(packet_id) & + prop::props_(props); + + auto message_ = + packet_type_ & + basic::varlen_(var_header_.byte_size() + reason_codes.size()) & + var_header_; + + auto s = encode(message_); + s.reserve(s.size() + reason_codes.size()); + + for (auto reason_code: reason_codes) + s << basic::byte_(reason_code); + + return s; +} + +inline std::string encode_unsubscribe( + uint16_t packet_id, + const std::vector& topics, + const unsubscribe_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b1010) | + basic::flag<4>(0b0010); + + size_t payload_size = 0; + for (const auto& topic: topics) + payload_size += basic::utf8_(topic).byte_size(); + + auto var_header_ = + basic::int16_(packet_id) & + prop::props_(props); + + auto message_ = + packet_type_ & + basic::varlen_(var_header_.byte_size() + payload_size) & + var_header_; + + auto s = encode(message_); + s.reserve(s.size() + payload_size); + + for (const auto& topic: topics) + s << basic::utf8_(topic); + + return s; +} + +inline std::string encode_unsuback( + uint16_t packet_id, + std::vector& reason_codes, + const unsuback_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b1011) | + basic::flag<4>(0b0000); + + auto var_header_ = + basic::int16_(packet_id) & + prop::props_(props); + + auto message_ = + packet_type_ & + basic::varlen_(var_header_.byte_size() + reason_codes.size()) & + var_header_; + + auto s = encode(message_); + s.reserve(s.size() + reason_codes.size()); + + for (auto reason_code: reason_codes) + s << basic::byte_(reason_code); + + return s; +} + +inline std::string encode_pingreq() { + auto packet_type_ = + basic::flag<4>(0b1100) | + basic::flag<4>(0); + + auto remaining_len_ = + basic::byte_(0); + + auto ping_req_ = packet_type_ & remaining_len_; + + return encode(ping_req_); +} + +inline std::string encode_pingresp() { + auto packet_type_ = + basic::flag<4>(0b1101) | + basic::flag<4>(0); + + auto remaining_len_ = + basic::byte_(0); + + auto ping_resp_ = packet_type_ & remaining_len_; + + return encode(ping_resp_); +} + +inline std::string encode_disconnect( + uint8_t reason_code, + const disconnect_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b1110) | + basic::flag<4>(0b0000); + + auto var_header_ = + basic::byte_(reason_code) & + prop::props_(props); + + auto fixed_header_ = + packet_type_ & + basic::varlen_(var_header_.byte_size()); + + auto disconnect_message_ = fixed_header_ & var_header_; + + return encode(disconnect_message_); +} + +inline std::string encode_auth( + uint8_t reason_code, + const auth_props& props +) { + + auto packet_type_ = + basic::flag<4>(0b1111) | + basic::flag<4>(0b0000); + + auto var_header_ = + basic::byte_(reason_code) & + prop::props_(props); + + auto fixed_header_ = + packet_type_ & + basic::varlen_(var_header_.byte_size()); + + auto auth_message_ = fixed_header_ & var_header_; + + return encode(auth_message_); +} + + +} // end namespace async_mqtt5::encoders + +#endif // !ASYNC_MQTT5_MESSAGE_ENCODERS_HPP diff --git a/include/async_mqtt5/impl/internal/codecs/traits.hpp b/include/async_mqtt5/impl/internal/codecs/traits.hpp new file mode 100644 index 0000000..1227ff1 --- /dev/null +++ b/include/async_mqtt5/impl/internal/codecs/traits.hpp @@ -0,0 +1,33 @@ +#ifndef ASYNC_MQTT5_TRAITS_HPP +#define ASYNC_MQTT5_TRAITS_HPP + +#include +#include + +#include + +namespace async_mqtt5 { + +template constexpr bool is_optional_impl = false; +template constexpr bool is_optional_impl> = true; + +template +constexpr bool is_optional = is_optional_impl>; + +template class> +constexpr bool is_specialization = false; + +template class T, class... Args> +constexpr bool is_specialization, T> = true; + +template +concept is_vector = is_specialization, std::vector>; + +template +concept is_boost_iterator = is_specialization< + std::remove_cvref_t, boost::iterator_range +>; + +} // end namespace async_mqtt5 + +#endif // !ASYNC_MQTT5_TRAITS_HPP diff --git a/include/async_mqtt5/impl/ping_op.hpp b/include/async_mqtt5/impl/ping_op.hpp new file mode 100644 index 0000000..7285f54 --- /dev/null +++ b/include/async_mqtt5/impl/ping_op.hpp @@ -0,0 +1,97 @@ +#ifndef ASYNC_MQTT5_PING_OP_HPP +#define ASYNC_MQTT5_PING_OP_HPP + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template +class ping_op { + using client_service = ClientService; + struct on_timer {}; + struct on_pingreq {}; + + static constexpr auto ping_interval = std::chrono::seconds(5); + + std::shared_ptr _svc_ptr; + std::unique_ptr _ping_timer; + +public: + ping_op( + const std::shared_ptr& svc_ptr + ) : + _svc_ptr(svc_ptr), + _ping_timer(new asio::steady_timer(svc_ptr->get_executor())) + {} + + ping_op(ping_op&&) noexcept = default; + ping_op(const ping_op&) = delete; + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc_ptr->get_executor(); + } + + using allocator_type = asio::recycling_allocator; + allocator_type get_allocator() const noexcept { + return allocator_type {}; + } + + using cancellation_slot_type = asio::cancellation_slot; + asio::cancellation_slot get_cancellation_slot() const noexcept { + return _svc_ptr->_cancel_ping.slot(); + } + + void perform(duration from_now) { + _ping_timer->expires_from_now(from_now); + _ping_timer->async_wait( + asio::prepend(std::move(*this), on_timer {}) + ); + } + + void operator()(on_timer, error_code ec) { + get_cancellation_slot().clear(); + + if (ec == asio::error::operation_aborted || !_svc_ptr->is_open()) + return; + + auto pingreq = control_packet::of( + no_pid, get_allocator(), encoders::encode_pingreq + ); + + const auto& wire_data = pingreq.wire_data(); + _svc_ptr->async_send( + wire_data, + no_serial, send_flag::none, + asio::consign( + asio::prepend(std::move(*this), on_pingreq {}), + std::move(pingreq) + ) + ); + } + + void operator()(on_pingreq, error_code ec) { + get_cancellation_slot().clear(); + + if (!ec || ec == asio::error::try_again) + perform(ping_interval - std::chrono::seconds(1)); + } +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_PING_OP_HPP diff --git a/include/async_mqtt5/impl/publish_rec_op.hpp b/include/async_mqtt5/impl/publish_rec_op.hpp new file mode 100644 index 0000000..d63f81b --- /dev/null +++ b/include/async_mqtt5/impl/publish_rec_op.hpp @@ -0,0 +1,204 @@ +#ifndef ASYNC_MQTT5_PUBLISH_REC_OP_HPP +#define ASYNC_MQTT5_PUBLISH_REC_OP_HPP + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template +class publish_rec_op { + using client_service = ClientService; + struct on_puback {}; + struct on_pubrec {}; + struct on_pubrel {}; + struct on_pubcomp {}; + + std::shared_ptr _svc_ptr; + decoders::publish_message _message; + +public: + publish_rec_op(const std::shared_ptr& svc_ptr) : + _svc_ptr(svc_ptr) + {} + + publish_rec_op(publish_rec_op&&) noexcept = default; + publish_rec_op(const publish_rec_op&) = delete; + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc_ptr->get_executor(); + } + + using allocator_type = asio::recycling_allocator; + allocator_type get_allocator() const noexcept { + return allocator_type {}; + } + + void perform(decoders::publish_message message) { + auto flags = std::get<2>(message); + auto qos_bits = (flags >> 1) & 0b11; + if (qos_bits == 0b11) + return on_malformed_packet( + "Malformed PUBLISH received: QoS bits set to 0b11" + ); + + auto qos = qos_e(qos_bits); + _message = std::move(message); + + if (qos == qos_e::at_most_once) + return complete(); + + auto packet_id = std::get<1>(_message); + + if (qos == qos_e::at_least_once) { + auto puback = control_packet::of( + with_pid, get_allocator(), + encoders::encode_puback, *packet_id, + uint8_t(0), puback_props{} + ); + return send_puback(std::move(puback)); + } + + // qos == qos_e::exactly_once + auto pubrec = control_packet::of( + with_pid, get_allocator(), + encoders::encode_pubrec, *packet_id, + uint8_t(0), pubrec_props{} + ); + + return send_pubrec(std::move(pubrec)); + } + + void send_puback(control_packet puback) { + const auto& wire_data = puback.wire_data(); + _svc_ptr->async_send( + wire_data, + no_serial, send_flag::none, + asio::consign( + asio::prepend(std::move(*this), on_puback {}), + std::move(puback) + ) + ); + } + + void operator()(on_puback, error_code ec) { + if (ec) + return; + + complete(); + } + + void send_pubrec(control_packet pubrec) { + const auto& wire_data = pubrec.wire_data(); + _svc_ptr->async_send( + wire_data, + no_serial, send_flag::none, + asio::prepend(std::move(*this), on_pubrec {}, std::move(pubrec)) + ); + } + + void operator()( + on_pubrec, control_packet packet, + error_code ec + ) { + if (ec) + return; + + wait_pubrel(packet.packet_id()); + } + + void wait_pubrel(uint16_t packet_id) { + _svc_ptr->async_wait_reply( + control_code_e::pubrel, packet_id, + asio::prepend(std::move(*this), on_pubrel {}, packet_id) + ); + } + + void operator()( + on_pubrel, uint16_t packet_id, + error_code ec, byte_citer first, byte_citer last + ) { + if (ec == asio::error::try_again) // "resend unanswered" + return wait_pubrel(packet_id); + + if (ec) + return; + + auto pubrel = decoders::decode_pubrel(std::distance(first, last), first); + if (!pubrel.has_value()) { + on_malformed_packet("Malformed PUBREL received: cannot decode"); + return wait_pubrel(packet_id); + } + + auto& [reason_code, props] = *pubrel; + auto rc = to_reason_code(reason_code); + if (!rc) { + on_malformed_packet("Malformed PUBREL received: invalid Reason Code"); + return wait_pubrel(packet_id); + } + + auto pubcomp = control_packet::of( + with_pid, get_allocator(), + encoders::encode_pubcomp, packet_id, + uint8_t(0), pubcomp_props{} + ); + send_pubcomp(std::move(pubcomp)); + } + + void send_pubcomp(control_packet pubcomp) { + const auto& wire_data = pubcomp.wire_data(); + _svc_ptr->async_send( + wire_data, + no_serial, send_flag::none, + asio::prepend(std::move(*this), on_pubcomp {}, std::move(pubcomp)) + ); + } + + void operator()( + on_pubcomp, control_packet packet, + error_code ec + ) { + if (ec == asio::error::try_again) + return wait_pubrel(packet.packet_id()); + + if (ec) + return; + + complete(); + } + +private: + void on_malformed_packet(const std::string& reason) { + auto props = disconnect_props {}; + props[prop::reason_string] = reason; + return async_disconnect( + disconnect_rc_e::malformed_packet, props, + false, _svc_ptr, asio::detached + ); + } + + void complete() { + // TODO: if rv == false then the channel buffer is full and + // there is no listener; we may need to log this + /* auto rv = */_svc_ptr->channel_store(std::move(_message)); + } +}; + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_PUBLISH_REC_OP_HPP diff --git a/include/async_mqtt5/impl/publish_send_op.hpp b/include/async_mqtt5/impl/publish_send_op.hpp new file mode 100644 index 0000000..623cbba --- /dev/null +++ b/include/async_mqtt5/impl/publish_send_op.hpp @@ -0,0 +1,383 @@ +#ifndef ASYNC_MQTT5_PUBLISH_SEND_OP_HPP +#define ASYNC_MQTT5_PUBLISH_SEND_OP_HPP + +#include + +#include + +#include +#include +#include +#include + +#include +#include + +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template +using on_publish_signature = std::conditional_t< + qos_type == qos_e::at_most_once, + void (error_code), + std::conditional_t< + qos_type == qos_e::at_least_once, + void (error_code, reason_code, puback_props), + void (error_code, reason_code, pubcomp_props) + > +>; + +template +using on_publish_props_type = std::conditional_t< + qos_type == qos_e::at_most_once, + void, + std::conditional_t< + qos_type == qos_e::at_least_once, + puback_props, + pubcomp_props + > +>; + +template +using cancel_args = std::conditional_t< + qos_type == qos_e::at_most_once, + std::tuple<>, + std::conditional_t< + qos_type == qos_e::at_least_once, + std::tuple, + std::tuple + > +>; + +template +class publish_send_op { + using client_service = ClientService; + + struct on_publish {}; + struct on_puback {}; + struct on_pubrec {}; + struct on_pubrel {}; + struct on_pubcomp {}; + + std::shared_ptr _svc_ptr; + + cancellable_handler< + Handler, + typename client_service::executor_type, + cancel_args + > _handler; + + serial_num_t _serial_num; + +public: + publish_send_op( + const std::shared_ptr& svc_ptr, Handler&& handler + ) : + _svc_ptr(svc_ptr), + _handler(std::move(handler), get_executor()) + {} + + publish_send_op(publish_send_op&&) noexcept = default; + publish_send_op(const publish_send_op&) = delete; + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc_ptr->get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + void perform( + std::string topic, std::string payload, + retain_e retain, const publish_props& props + ) { + auto ec = validate_publish(retain, props); + if (ec) + return complete_post(ec); + + uint16_t packet_id = 0; + if constexpr (qos_type != qos_e::at_most_once) { + packet_id = _svc_ptr->allocate_pid(); + if (packet_id == 0) + return complete_post(client::error::pid_overrun); + } + + _serial_num = _svc_ptr->next_serial_num(); + + auto publish = control_packet::of( + with_pid, get_allocator(), + encoders::encode_publish, packet_id, + std::move(topic), std::move(payload), + qos_type, retain, dup_e::no, props + ); + + send_publish(std::move(publish)); + } + + error_code validate_publish( + retain_e retain, const publish_props& props + ) { + auto max_qos = _svc_ptr->connack_prop(prop::maximum_qos); + if (max_qos && uint8_t(qos_type) > *max_qos) + return client::error::qos_not_supported; + + auto retain_available = _svc_ptr->connack_prop(prop::retain_available); + if (retain_available && *retain_available == 0 && retain == retain_e::yes) + return client::error::retain_not_available; + + // TODO: topic alias mapping + auto topic_alias_max = _svc_ptr->connack_prop(prop::topic_alias_maximum); + auto topic_alias = props[prop::topic_alias]; + if ((!topic_alias_max || topic_alias_max && *topic_alias_max == 0) && topic_alias) + return client::error::topic_alias_maximum_reached; + if (topic_alias_max && topic_alias && *topic_alias > *topic_alias_max) + return client::error::topic_alias_maximum_reached; + return {}; + } + + void send_publish(control_packet publish) { + const auto& wire_data = publish.wire_data(); + _svc_ptr->async_send( + wire_data, + _serial_num, + send_flag::throttled * (qos_type != qos_e::at_most_once), + asio::prepend(std::move(*this), on_publish {}, std::move(publish)) + ); + } + + void operator()( + on_publish, control_packet publish, + error_code ec + ) { + if (ec == asio::error::try_again) + return send_publish(std::move(publish)); + + if constexpr (qos_type == qos_e::at_most_once) + return complete(ec); + + else { + auto packet_id = publish.packet_id(); + + if constexpr (qos_type == qos_e::at_least_once) { + if (ec) + return complete( + ec, reason_codes::empty, packet_id, puback_props {} + ); + _svc_ptr->async_wait_reply( + control_code_e::puback, packet_id, + asio::prepend( + std::move(*this), on_puback {}, std::move(publish) + ) + ); + } + + else if constexpr (qos_type == qos_e::exactly_once) { + if (ec) + return complete( + ec, reason_codes::empty, packet_id, pubcomp_props {} + ); + _svc_ptr->async_wait_reply( + control_code_e::pubrec, packet_id, + asio::prepend( + std::move(*this), on_pubrec {}, std::move(publish) + ) + ); + } + } + } + + void operator()( + on_puback, control_packet publish, + error_code ec, byte_citer first, byte_citer last + ) + requires (qos_type == qos_e::at_least_once) { + + if (ec == asio::error::try_again) // "resend unanswered" + return send_publish(std::move(publish.set_dup())); + + uint16_t packet_id = publish.packet_id(); + + if (ec) + return complete( + ec, reason_codes::empty, packet_id, puback_props {} + ); + + auto puback = decoders::decode_puback(std::distance(first, last), first); + if (!puback.has_value()) { + on_malformed_packet("Malformed PUBACK: cannot decode"); + return send_publish(std::move(publish.set_dup())); + } + + auto& [reason_code, props] = *puback; + auto rc = to_reason_code(reason_code); + if (!rc) { + on_malformed_packet("Malformed PUBACK: invalid Reason Code"); + return send_publish(std::move(publish.set_dup())); + } + + complete(ec, *rc, packet_id, std::move(props)); + } + + void operator()( + on_pubrec, control_packet publish, + error_code ec, byte_citer first, byte_citer last + ) + requires (qos_type == qos_e::exactly_once) { + + if (ec == asio::error::try_again) // "resend unanswered" + return send_publish(std::move(publish.set_dup())); + + uint16_t packet_id = publish.packet_id(); + + if (ec) + return complete( + ec, reason_codes::empty, packet_id, pubcomp_props {} + ); + + auto pubrec = decoders::decode_pubrec(std::distance(first, last), first); + if (!pubrec.has_value()) { + on_malformed_packet("Malformed PUBREC: cannot decode"); + return send_publish(std::move(publish.set_dup())); + } + + auto& [reason_code, props] = *pubrec; + + auto rc = to_reason_code(reason_code); + if (!rc) { + on_malformed_packet("Malformed PUBREC: invalid Reason Code"); + return send_publish(std::move(publish.set_dup())); + } + + if (*rc) + return complete(ec, *rc, packet_id, pubcomp_props {}); + + auto pubrel = control_packet::of( + with_pid, get_allocator(), + encoders::encode_pubrel, packet_id, + 0, pubrel_props {} + ); + + send_pubrel(std::move(pubrel), false); + } + + void send_pubrel(control_packet pubrel, bool throttled) { + const auto& wire_data = pubrel.wire_data(); + _svc_ptr->async_send( + wire_data, + _serial_num, + (send_flag::throttled * throttled) | send_flag::prioritized, + asio::prepend(std::move(*this), on_pubrel {}, std::move(pubrel)) + ); + } + + void operator()( + on_pubrel, control_packet pubrel, error_code ec + ) + requires (qos_type == qos_e::exactly_once) { + + if (ec == asio::error::try_again) + return send_pubrel(std::move(pubrel), true); + + uint16_t packet_id = pubrel.packet_id(); + + if (ec) + return complete( + ec, reason_codes::empty, packet_id, pubcomp_props {} + ); + + _svc_ptr->async_wait_reply( + control_code_e::pubcomp, packet_id, + asio::prepend(std::move(*this), on_pubcomp {}, std::move(pubrel)) + ); + } + + void operator()( + on_pubcomp, control_packet pubrel, + error_code ec, + byte_citer first, byte_citer last + ) + requires (qos_type == qos_e::exactly_once) { + + if (ec == asio::error::try_again) // "resend unanswered" + return send_pubrel(std::move(pubrel), true); + + uint16_t packet_id = pubrel.packet_id(); + + if (ec) + return complete( + ec, reason_codes::empty, packet_id, pubcomp_props {} + ); + + auto pubcomp = decoders::decode_pubcomp(std::distance(first, last), first); + if (!pubcomp.has_value()) { + on_malformed_packet("Malformed PUBCOMP: cannot decode"); + return send_pubrel(std::move(pubrel), true); + } + + auto& [reason_code, props] = *pubcomp; + + auto rc = to_reason_code(reason_code); + if (!rc) { + on_malformed_packet("Malformed PUBCOMP: invalid Reason Code"); + return send_pubrel(std::move(pubrel), true); + } + + return complete(ec, *rc, pubrel.packet_id(), pubcomp_props{}); + } + + +private: + void on_malformed_packet(const std::string& reason) { + auto props = disconnect_props {}; + props[prop::reason_string] = reason; + async_disconnect( + disconnect_rc_e::malformed_packet, props, false, _svc_ptr, + asio::detached + ); + } + + void complete(error_code ec) + requires (qos_type == qos_e::at_most_once) + { + _handler.complete(ec); + } + + void complete_post(error_code ec) + requires (qos_type == qos_e::at_most_once) + { + _handler.complete_post(ec); + } + + template > + requires ( + std::is_same_v || + std::is_same_v + ) + void complete( + error_code ec, reason_code rc, + uint16_t packet_id, Props&& props + ) { + _svc_ptr->free_pid(packet_id, true); + _handler.complete(ec, rc, std::forward(props)); + } + + template > + requires ( + std::is_same_v || + std::is_same_v + ) + void complete_post(error_code ec) { + _handler.complete_post(ec, reason_codes::empty, Props {}); + } +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_PUBLISH_SEND_OP_HPP diff --git a/include/async_mqtt5/impl/read_message_op.hpp b/include/async_mqtt5/impl/read_message_op.hpp new file mode 100644 index 0000000..f1fb4a2 --- /dev/null +++ b/include/async_mqtt5/impl/read_message_op.hpp @@ -0,0 +1,127 @@ +#ifndef ASYNC_MQTT5_READ_MESSAGE_OP_HPP +#define ASYNC_MQTT5_READ_MESSAGE_OP_HPP + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template +class read_message_op { + using client_service = ClientService; + struct on_message {}; + struct on_disconnect {}; + + std::shared_ptr _svc_ptr; +public: + read_message_op( + const std::shared_ptr& svc_ptr + ) : + _svc_ptr(svc_ptr) + {} + + read_message_op(read_message_op&&) noexcept = default; + read_message_op(const read_message_op&) = delete; + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc_ptr->get_executor(); + } + + using allocator_type = asio::recycling_allocator; + allocator_type get_allocator() const noexcept { + return allocator_type {}; + } + + void perform() { + _svc_ptr->async_assemble( + std::chrono::seconds(20), + asio::prepend(std::move(*this), on_message {}) + ); + } + + void operator()( + on_message, error_code ec, + uint16_t packet_id, uint8_t control_code, + byte_citer first, byte_citer last + ) { + if (ec == client::error::malformed_packet) + return on_malformed_packet( + "Malformed Packet received from the Server" + ); + + if ( + ec == asio::error::operation_aborted || + ec == asio::error::no_recovery + ) + return; + + dispatch(ec, packet_id, control_code, first, last); + } + + void operator()(on_disconnect, error_code ec) { + if (!ec || ec == asio::error::try_again) + perform(); + } + +private: + + void dispatch( + error_code ec, uint16_t packet_id, uint8_t control_byte, + byte_citer first, byte_citer last + ) { + using enum control_code_e; + auto code = control_code_e(control_byte & 0b11110000); + + switch (code) { + case publish: { + auto msg = decoders::decode_publish( + control_byte, std::distance(first, last), first + ); + if (!msg.has_value()) + return on_malformed_packet( + "Malformed PUBLISH received: cannot decode" + ); + + publish_rec_op { _svc_ptr }.perform(std::move(*msg)); + } + break; + case disconnect: { + _svc_ptr->close_stream(); + _svc_ptr->open_stream(); + } + break; + case auth: { + // TODO: dispatch auth + } + break; + } + perform(); + } + + void on_malformed_packet(const std::string& reason) { + auto props = disconnect_props {}; + props[prop::reason_string] = reason; + auto svc_ptr = _svc_ptr; // copy before this is moved + async_disconnect( + disconnect_rc_e::malformed_packet, props, false, svc_ptr, + asio::prepend(std::move(*this), on_disconnect {}) + ); + } + +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_READ_MESSAGE_OP_HPP diff --git a/include/async_mqtt5/impl/read_op.hpp b/include/async_mqtt5/impl/read_op.hpp new file mode 100644 index 0000000..9b28e8e --- /dev/null +++ b/include/async_mqtt5/impl/read_op.hpp @@ -0,0 +1,117 @@ +#ifndef ASYNC_MQTT5_READ_OP_HPP +#define ASYNC_MQTT5_READ_OP_HPP + +#include +#include +#include + +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; +namespace asioex = boost::asio::experimental; + +template +class read_op { + struct on_read {}; + struct on_reconnect {}; + + Owner& _owner; + std::decay_t _handler; + +public: + read_op(Owner& owner, Handler&& handler) : + _owner(owner), + _handler(std::move(handler)) + {} + + read_op(read_op&&) noexcept = default; + read_op(const read_op&) = delete; + + using executor_type = typename Owner::executor_type; + executor_type get_executor() const noexcept { + return _owner.get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + template + void perform( + const BufferType& buffer, duration wait_for + ) { + auto stream_ptr = _owner._stream_ptr; + + if (_owner.was_connected()) { + _owner._read_timer.expires_from_now(wait_for); + + auto timed_read = asioex::make_parallel_group( + stream_ptr->async_read_some(buffer, asio::deferred), + _owner._read_timer.async_wait(asio::deferred) + ); + + timed_read.async_wait( + asioex::wait_for_one(), + asio::prepend(std::move(*this), on_read {}, stream_ptr) + ); + + } + else + (*this)( + on_read {}, stream_ptr, + { 0, 1 }, asio::error::not_connected, 0, {} + ); + } + + void operator()( + on_read, typename Owner::stream_ptr stream_ptr, + std::array ord, error_code read_ec, size_t bytes_read, + error_code timer_ec + ) { + if (!_owner.is_open()) + return complete(asio::error::operation_aborted, bytes_read); + + error_code ec = ord[0] == 1 ? asio::error::timed_out : read_ec; + bytes_read = ord[0] == 0 ? bytes_read : 0; + + if (!ec) + return complete(ec, bytes_read); + + // websocket returns operation_aborted if disconnected + if (should_reconnect(ec) || ec == asio::error::operation_aborted) + return _owner.async_reconnect( + stream_ptr, asio::prepend(std::move(*this), on_reconnect {}) + ); + + return complete(asio::error::no_recovery, bytes_read); + } + + void operator()(on_reconnect, error_code ec) { + if ((ec == asio::error::operation_aborted && _owner.is_open()) || !ec) + ec = asio::error::try_again; + + return complete(ec, 0); + } + +private: + void complete(error_code ec, size_t bytes_read) { + asio::dispatch( + get_executor(), + asio::prepend(std::move(_handler), ec, bytes_read) + ); + } + + static bool should_reconnect(error_code ec) { + using namespace asio::error; + return ec == connection_aborted || ec == not_connected || + ec == timed_out || ec == connection_reset || ec == broken_pipe; + } +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_READ_OP_HPP diff --git a/include/async_mqtt5/impl/reconnect_op.hpp b/include/async_mqtt5/impl/reconnect_op.hpp new file mode 100644 index 0000000..c43cbc7 --- /dev/null +++ b/include/async_mqtt5/impl/reconnect_op.hpp @@ -0,0 +1,190 @@ +#ifndef ASYNC_MQTT5_RECONNECT_OP_HPP +#define ASYNC_MQTT5_RECONNECT_OP_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template +class reconnect_op { + struct on_locked {}; + struct on_next_endpoint {}; + struct on_connect {}; + struct on_backoff {}; + + Owner& _owner; + std::decay_t _handler; + std::unique_ptr _buffer_ptr; + + using endpoint = asio::ip::tcp::endpoint; + using epoints = asio::ip::tcp::resolver::results_type; + +public: + reconnect_op(Owner& owner, Handler&& handler) : + _owner(owner), + _handler(std::move(handler)) + {} + + reconnect_op(reconnect_op&&) noexcept = default; + reconnect_op(const reconnect_op&) = delete; + + using executor_type = typename Owner::executor_type; + executor_type get_executor() const noexcept { + return _owner.get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + using cancellation_slot_type = + asio::associated_cancellation_slot_t; + cancellation_slot_type get_cancellation_slot() const noexcept { + return asio::get_associated_cancellation_slot(_handler); + } + + void perform(typename Owner::stream_ptr s) { + _owner._conn_mtx.lock( + asio::prepend(std::move(*this), on_locked {}, s) + ); + } + + void operator()(on_locked, typename Owner::stream_ptr s, error_code ec) { + if (ec == asio::error::operation_aborted || !_owner.is_open()) + return complete(asio::error::operation_aborted); + + if (s != _owner._stream_ptr) + return complete(asio::error::try_again); + + do_reconnect(); + } + + void do_reconnect() { + _owner._endpoints.async_next_endpoint( + asio::prepend(std::move(*this), on_next_endpoint {}) + ); + } + + void backoff_and_reconnect() { + _owner._connect_timer.expires_from_now(std::chrono::seconds(5)); + _owner._connect_timer.async_wait( + asio::prepend(std::move(*this), on_backoff {}) + ); + } + + void operator()(on_backoff, error_code ec) { + if (ec == asio::error::operation_aborted || !_owner.is_open()) + return complete(asio::error::operation_aborted); + + do_reconnect(); + } + + void operator()( + on_next_endpoint, error_code ec, + epoints eps, authority_path ap + ) { + namespace asioex = boost::asio::experimental; + + // the three error codes below are the only possible codes + // that may be returned from async_next_endpont + + if (ec == asio::error::operation_aborted || !_owner.is_open()) + return complete(asio::error::operation_aborted); + + if (ec == asio::error::try_again) + return backoff_and_reconnect(); + + if (ec == asio::error::host_not_found) + return complete(asio::error::no_recovery); + + auto sptr = _owner.construct_next_layer(); + + if constexpr (has_tls_context) + setup_tls_sni( + ap, _owner._stream_context.tls_context(), *sptr + ); + + // wait max 5 seconds for the connect (handshake) op to finish + _owner._connect_timer.expires_from_now(std::chrono::seconds(5)); + + auto init_connect = [this, sptr]( + auto handler, const auto& eps, auto ap + ) { + connect_op { + *sptr, std::move(handler), + _owner._stream_context.mqtt_context() + }.perform(eps, std::move(ap)); + }; + + auto timed_connect = asioex::make_parallel_group( + asio::async_initiate( + std::move(init_connect), asio::deferred, + std::move(eps), std::move(ap) + ), + _owner._connect_timer.async_wait(asio::deferred) + ); + + timed_connect.async_wait( + asioex::wait_for_one(), + asio::prepend(std::move(*this), on_connect {}, std::move(sptr)) + ); + } + + void operator()( + on_connect, typename Owner::stream_ptr sptr, + auto ord, error_code connect_ec, error_code timer_ec + ) { + // connect_ec may be any of stream.async_connect() error codes + // plus access_denied, connection_refused and + // client::error::malformed_packet + if ( + ord[0] == 0 && connect_ec == asio::error::operation_aborted || + ord[0] == 1 && timer_ec == asio::error::operation_aborted || + !_owner.is_open() + ) + return complete(asio::error::operation_aborted); + + // operation timed out so retry + if (ord[0] == 1) + return do_reconnect(); + + if (connect_ec == asio::error::access_denied) + return complete(asio::error::no_recovery); + + // retry for any other stream.async_connect() error or + // connection_refused, client::error::malformed_packet + if (connect_ec) + return do_reconnect(); + + _owner.replace_next_layer(std::move(sptr)); + complete(error_code {}); + } + +private: + void complete(error_code ec) { + get_cancellation_slot().clear(); + _owner._conn_mtx.unlock(); + + asio::dispatch( + get_executor(), + asio::prepend(std::move(_handler), ec) + ); + } + +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_RECONNECT_OP_HPP diff --git a/include/async_mqtt5/impl/replies.hpp b/include/async_mqtt5/impl/replies.hpp new file mode 100644 index 0000000..f755643 --- /dev/null +++ b/include/async_mqtt5/impl/replies.hpp @@ -0,0 +1,186 @@ +#ifndef ASYNC_MQTT5_REPLIES_HPP +#define ASYNC_MQTT5_REPLIES_HPP + +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +class replies { + using signature = void (error_code, byte_citer, byte_citer); + + static constexpr auto max_reply_time = std::chrono::seconds(20); + + class handler_type : public asio::any_completion_handler { + using base = asio::any_completion_handler; + control_code_e _code; + uint16_t _packet_id; + std::chrono::time_point _ts; + public: + template + handler_type(control_code_e code, uint16_t pid, H&& handler) : + base(std::forward(handler)), _code(code), _packet_id(pid), + _ts(std::chrono::system_clock::now()) + {} + + handler_type(handler_type&& other) noexcept : + base(static_cast(other)), + _code(other._code), _packet_id(other._packet_id), _ts(other._ts) + {} + + handler_type& operator=(handler_type&& other) noexcept { + base::operator=(static_cast(other)); + _code = other._code; + _packet_id = other._packet_id; + _ts = other._ts; + return *this; + } + + uint16_t packet_id() const noexcept { + return _packet_id; + } + + control_code_e code() const noexcept { + return _code; + } + + auto time() const noexcept { + return _ts; + } + }; + + using handlers = std::vector; + handlers _handlers; + + struct fast_reply { + control_code_e code; + uint16_t packet_id; + std::unique_ptr packet; + }; + using fast_replies = std::vector; + fast_replies _fast_replies; + +public: + template + decltype(auto) async_wait_reply( + control_code_e code, uint16_t packet_id, CompletionToken&& token + ) { + auto dup_handler_ptr = find_handler(code, packet_id); + if (dup_handler_ptr != _handlers.end()) { + std::move(*dup_handler_ptr)( + asio::error::operation_aborted, byte_citer {}, byte_citer {} + ); + _handlers.erase(dup_handler_ptr); + } + + auto freply = find_fast_reply(code, packet_id); + if (freply == _fast_replies.end()) { + auto initiate = [this]( + auto handler, control_code_e code, uint16_t packet_id + ) { + _handlers.emplace_back(code, packet_id, std::move(handler)); + }; + return asio::async_initiate( + std::move(initiate), token, code, packet_id + ); + } + + auto fdata = std::move(*freply); + _fast_replies.erase(freply); + + byte_citer first = fdata.packet->cbegin(), last = fdata.packet->cend(); + auto with_packet = asio::consign( + std::forward(token), std::move(fdata.packet) + ); + auto initiate = [](auto handler, byte_citer first, byte_citer last) { + auto ex = asio::get_associated_executor(handler); + asio::post(ex, [h = std::move(handler), first, last]() mutable { + std::move(h)(error_code {}, first, last); + }); + }; + return asio::async_initiate( + std::move(initiate), with_packet, first, last + ); + } + + void dispatch( + error_code ec, control_code_e code, uint16_t packet_id, + byte_citer first, byte_citer last + ) { + auto handler_ptr = find_handler(code, packet_id); + if (handler_ptr == _handlers.end()) { + _fast_replies.push_back({ + code, packet_id, std::make_unique(first, last) + }); + return; + } + auto handler = std::move(*handler_ptr); + _handlers.erase(handler_ptr); + std::move(handler)(ec, first, last); + } + + void resend_unanswered() { + auto ua = std::move(_handlers); + for (auto& h : ua) + std::move(h)(asio::error::try_again, byte_citer {}, byte_citer {}); + } + + void cancel_unanswered() { + auto ua = std::move(_handlers); + for (auto& h : ua) + std::move(h)( + asio::error::operation_aborted, + byte_citer {}, byte_citer {} + ); + } + + bool any_expired() { + auto now = std::chrono::system_clock::now(); + return std::any_of( + _handlers.begin(), _handlers.end(), + [now](const auto& h) { + return now - h.time() > max_reply_time; + } + ); + } + + void clear_fast_replies() { + _fast_replies.clear(); + } + +private: + handlers::iterator find_handler(control_code_e code, uint16_t packet_id) { + return std::find_if( + _handlers.begin(), _handlers.end(), + [code, packet_id](const auto& h) { + return h.code() == code && h.packet_id() == packet_id; + } + ); + } + + fast_replies::iterator find_fast_reply( + control_code_e code, uint16_t packet_id + ) { + return std::find_if( + _fast_replies.begin(), _fast_replies.end(), + [code, packet_id](const auto& f) { + return f.code == code && f.packet_id == packet_id; + } + ); + } + +}; + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_REPLIES_HPP diff --git a/include/async_mqtt5/impl/sentry_op.hpp b/include/async_mqtt5/impl/sentry_op.hpp new file mode 100644 index 0000000..22719f1 --- /dev/null +++ b/include/async_mqtt5/impl/sentry_op.hpp @@ -0,0 +1,91 @@ +#ifndef ASYNC_MQTT5_SENTRY_OP_HPP +#define ASYNC_MQTT5_SENTRY_OP_HPP + +#include +#include +#include +#include +#include + +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template +class sentry_op { + using client_service = ClientService; + struct on_timer {}; + struct on_disconnect {}; + + static constexpr auto check_interval = std::chrono::seconds(3); + + std::shared_ptr _svc_ptr; + std::unique_ptr _sentry_timer; + +public: + sentry_op( + const std::shared_ptr& svc_ptr + ) : + _svc_ptr(svc_ptr), + _sentry_timer(new asio::steady_timer(svc_ptr->get_executor())) + {} + + sentry_op(sentry_op&&) noexcept = default; + sentry_op(const sentry_op&) = delete; + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc_ptr->get_executor(); + } + + using allocator_type = asio::recycling_allocator; + allocator_type get_allocator() const noexcept { + return allocator_type {}; + } + + using cancellation_slot_type = asio::cancellation_slot; + asio::cancellation_slot get_cancellation_slot() const noexcept { + return _svc_ptr->_cancel_sentry.slot(); + } + + void perform() { + _sentry_timer->expires_after(check_interval); + _sentry_timer->async_wait( + asio::prepend(std::move(*this), on_timer {}) + ); + } + + void operator()(on_timer, error_code ec) { + get_cancellation_slot().clear(); + + if (ec == asio::error::operation_aborted || !_svc_ptr->is_open()) + return; + + if (_svc_ptr->_replies.any_expired()) { + auto props = disconnect_props{}; + // TODO add what packet was expected? + props[prop::reason_string] = "No reply received within 20 seconds"; + auto svc_ptr = _svc_ptr; + return async_disconnect( + disconnect_rc_e::unspecified_error, props, false, svc_ptr, + asio::prepend(std::move(*this), on_disconnect {}) + ); + } + + perform(); + } + + void operator()(on_disconnect, error_code ec) { + get_cancellation_slot().clear(); + + if (!ec || ec == asio::error::try_again) + perform(); + } +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_SENTRY_OP_HPP diff --git a/include/async_mqtt5/impl/subscribe_op.hpp b/include/async_mqtt5/impl/subscribe_op.hpp new file mode 100644 index 0000000..c56a7e5 --- /dev/null +++ b/include/async_mqtt5/impl/subscribe_op.hpp @@ -0,0 +1,171 @@ +#ifndef ASYNC_MQTT5_SUBSCRIBE_OP_HPP +#define ASYNC_MQTT5_SUBSCRIBE_OP_HPP + +#include + +#include + +#include +#include +#include +#include + +#include +#include + +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template +class subscribe_op { + using client_service = ClientService; + struct on_subscribe {}; + struct on_suback {}; + + std::shared_ptr _svc_ptr; + + cancellable_handler< + Handler, + typename client_service::executor_type, + std::tuple, suback_props> + > _handler; + +public: + subscribe_op( + const std::shared_ptr& svc_ptr, Handler&& handler + ) : + _svc_ptr(svc_ptr), + _handler(std::move(handler), get_executor()) + {} + + subscribe_op(subscribe_op&&) noexcept = default; + subscribe_op(const subscribe_op&) noexcept = delete; + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc_ptr->get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + void perform( + const std::vector& topics, + const subscribe_props& props + ) { + uint16_t packet_id = _svc_ptr->allocate_pid(); + if (packet_id == 0) + return complete_post(client::error::pid_overrun); + + auto subscribe = control_packet::of( + with_pid, get_allocator(), + encoders::encode_subscribe, packet_id, + topics, props + ); + + send_subscribe(std::move(subscribe)); + } + + void send_subscribe(control_packet subscribe) { + const auto& wire_data = subscribe.wire_data(); + _svc_ptr->async_send( + wire_data, + no_serial, send_flag::none, + asio::prepend( + std::move(*this), on_subscribe {}, std::move(subscribe) + ) + ); + } + + void operator()( + on_subscribe, control_packet packet, + error_code ec + ) { + if (ec == asio::error::try_again) + return send_subscribe(std::move(packet)); + + auto packet_id = packet.packet_id(); + + if (ec) + return complete(ec, packet_id, {}, {}); + + _svc_ptr->async_wait_reply( + control_code_e::suback, packet_id, + asio::prepend(std::move(*this), on_suback{}, std::move(packet)) + ); + } + + void operator()( + on_suback, control_packet packet, + error_code ec, byte_citer first, byte_citer last + ) { + if (ec == asio::error::try_again) // "resend unanswered" + return send_subscribe(std::move(packet)); + + uint16_t packet_id = packet.packet_id(); + + if (ec) + return complete(ec, packet_id, {}, {}); + + auto suback = decoders::decode_suback(std::distance(first, last), first); + if (!suback.has_value()) { + on_malformed_packet("Malformed SUBACK: cannot decode"); + return send_subscribe(std::move(packet)); + } + + auto& [props, reason_codes] = *suback; + // TODO: perhaps do something with the topics we subscribed to (one day) + + complete( + ec, packet_id, + to_reason_codes(std::move(reason_codes)), std::move(props) + ); + } + +private: + + static std::vector to_reason_codes(std::vector codes) { + std::vector ret; + for (uint8_t code : codes) { + auto rc = to_reason_code(code); + if (rc) + ret.push_back(*rc); + // TODO: on off chance that one of the rcs is invalid, should we push something to mark that? + } + return ret; + } + + void on_malformed_packet(const std::string& reason) { + auto props = disconnect_props{}; + props[prop::reason_string] = reason; + async_disconnect( + disconnect_rc_e::malformed_packet, props, false, _svc_ptr, + asio::detached + ); + } + + + void complete_post(error_code ec) { + _handler.complete_post( + ec, std::vector {}, suback_props {} + ); + } + + void complete( + error_code ec, uint16_t packet_id, + std::vector reason_codes, suback_props props + ) { + _svc_ptr->free_pid(packet_id); + _handler.complete(ec, std::move(reason_codes), std::move(props)); + } +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_SUBSCRIBE_OP_HPP diff --git a/include/async_mqtt5/impl/unsubscribe_op.hpp b/include/async_mqtt5/impl/unsubscribe_op.hpp new file mode 100644 index 0000000..40f5755 --- /dev/null +++ b/include/async_mqtt5/impl/unsubscribe_op.hpp @@ -0,0 +1,173 @@ +#ifndef ASYNC_MQTT5_UNSUBSCRIBE_OP_HPP +#define ASYNC_MQTT5_UNSUBSCRIBE_OP_HPP + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace async_mqtt5::detail { + +namespace asio = boost::asio; + +template +class unsubscribe_op { + using client_service = ClientService; + struct on_unsubscribe {}; + struct on_unsuback {}; + + std::shared_ptr _svc_ptr; + + cancellable_handler< + Handler, + typename client_service::executor_type, + std::tuple, unsuback_props> + > _handler; + +public: + unsubscribe_op( + const std::shared_ptr& svc_ptr, Handler&& handler + ) : + _svc_ptr(svc_ptr), + _handler(std::move(handler), get_executor()) + {} + + unsubscribe_op(unsubscribe_op&&) noexcept = default; + unsubscribe_op(const unsubscribe_op&) noexcept = delete; + + using executor_type = typename client_service::executor_type; + executor_type get_executor() const noexcept { + return _svc_ptr->get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + void perform( + const std::vector& topics, + const unsubscribe_props& props + ) { + uint16_t packet_id = _svc_ptr->allocate_pid(); + if (packet_id == 0) + return complete_post(client::error::pid_overrun); + + auto unsubscribe = control_packet::of( + with_pid, get_allocator(), + encoders::encode_unsubscribe, packet_id, + topics, props + ); + + send_unsubscribe(std::move(unsubscribe)); + } + + void send_unsubscribe(control_packet unsubscribe) { + const auto& wire_data = unsubscribe.wire_data(); + _svc_ptr->async_send( + wire_data, + no_serial, send_flag::none, + asio::prepend( + std::move(*this), on_unsubscribe{}, std::move(unsubscribe) + ) + ); + } + + void operator()( + on_unsubscribe, control_packet packet, + error_code ec + ) { + if (ec == asio::error::try_again) + return send_unsubscribe(std::move(packet)); + + auto packet_id = packet.packet_id(); + + if (ec) + return complete(ec, packet_id, {}, {}); + + _svc_ptr->async_wait_reply( + control_code_e::unsuback, packet_id, + asio::prepend(std::move(*this), on_unsuback{}, std::move(packet)) + ); + } + + void operator()( + on_unsuback, control_packet packet, + error_code ec, byte_citer first, byte_citer last + ) { + if (ec == asio::error::try_again) // "resend unanswered" + return send_unsubscribe(std::move(packet)); + + uint16_t packet_id = packet.packet_id(); + + if (ec) + return complete(ec, packet_id, {}, {}); + + auto unsuback = decoders::decode_unsuback( + std::distance(first, last), first + ); + if (!unsuback.has_value()) { + on_malformed_packet("Malformed UNSUBACK: cannot decode"); + return send_unsubscribe(std::move(packet)); + } + + auto& [props, reason_codes] = *unsuback; + // TODO: perhaps do something with the topics we unsubscribed from (one day) + + complete( + ec, packet_id, + to_reason_codes(std::move(reason_codes)), std::move(props) + ); + } + +private: + + static std::vector to_reason_codes(std::vector codes) { + std::vector ret; + for (uint8_t code : codes) { + auto rc = to_reason_code(code); + if (rc) + ret.push_back(*rc); + // TODO: on off chance that one of the rcs is invalid, should we push something to mark that? + } + return ret; + } + + void on_malformed_packet( + const std::string& reason + ) { + auto props = disconnect_props{}; + props[prop::reason_string] = reason; + async_disconnect( + disconnect_rc_e::malformed_packet, props, false, _svc_ptr, + asio::detached + ); + } + + void complete_post(error_code ec) { + _handler.complete_post( + ec, std::vector {}, unsuback_props {} + ); + } + + void complete( + error_code ec, uint16_t packet_id, + std::vector reason_codes, unsuback_props props + ) { + _svc_ptr->free_pid(packet_id); + _handler.complete(ec, std::move(reason_codes), std::move(props)); + } +}; + + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_UNSUBSCRIBE_OP_HPP diff --git a/include/async_mqtt5/impl/write_op.hpp b/include/async_mqtt5/impl/write_op.hpp new file mode 100644 index 0000000..2bdd5bf --- /dev/null +++ b/include/async_mqtt5/impl/write_op.hpp @@ -0,0 +1,100 @@ +#ifndef ASYNC_MQTT5_WRITE_OP_HPP +#define ASYNC_MQTT5_WRITE_OP_HPP + +#include +#include +#include + +#include +#include + +namespace async_mqtt5::detail { + +template +class write_op { + struct on_write_locked {}; + struct on_write {}; + struct on_reconnect {}; + + Owner& _owner; + std::decay_t _handler; + +public: + write_op( + Owner& owner, Handler&& handler) : + _owner(owner), + _handler(std::forward(handler)) + {} + + write_op(write_op&&) noexcept = default; + write_op(const write_op&) = delete; + + using executor_type = typename Owner::executor_type; + executor_type get_executor() const noexcept { + return _owner.get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + template + void perform(BufferType& buffer) { + auto stream_ptr = _owner._stream_ptr; + if (_owner.was_connected()) + // note: write operation should not be time-limited + detail::async_write( + *stream_ptr, buffer, + asio::prepend(std::move(*this), on_write {}, stream_ptr) + ); + else + (*this)(on_write {}, stream_ptr, asio::error::not_connected, 0); + } + + void operator()( + on_write, typename Owner::stream_ptr stream_ptr, + error_code ec, size_t bytes_written + ) { + if (!_owner.is_open()) + return complete(asio::error::operation_aborted, 0); + + if (!ec) + return complete(ec, bytes_written); + + // websocket returns operation_aborted if disconnected + if (should_reconnect(ec) || ec == asio::error::operation_aborted) + return _owner.async_reconnect( + stream_ptr, asio::prepend(std::move(*this), on_reconnect {}) + ); + + return complete(asio::error::no_recovery, 0); + } + + void operator()(on_reconnect, error_code ec) { + if ((ec == asio::error::operation_aborted && _owner.is_open()) || !ec) + ec = asio::error::try_again; + + return complete(ec, 0); + } + +private: + void complete(error_code ec, size_t bytes_written) { + asio::dispatch( + get_executor(), + asio::prepend(std::move(_handler), ec, bytes_written) + ); + } + + static bool should_reconnect(error_code ec) { + using namespace asio::error; + return ec == connection_aborted || ec == not_connected + || ec == timed_out || ec == connection_reset + || ec == broken_pipe || ec == asio::error::eof; + } + +}; + +} // end namespace async_mqtt5::detail + +#endif // !ASYNC_MQTT5_WRITE_OP_HPP diff --git a/include/async_mqtt5/mqtt_client.hpp b/include/async_mqtt5/mqtt_client.hpp new file mode 100644 index 0000000..132b4ed --- /dev/null +++ b/include/async_mqtt5/mqtt_client.hpp @@ -0,0 +1,230 @@ +#ifndef ASYNC_MQTT5_MQTT_CLIENT_HPP +#define ASYNC_MQTT5_MQTT_CLIENT_HPP + +#include +#include +#include +#include +#include + +namespace async_mqtt5 { + +namespace asio = boost::asio; + +template < + typename StreamType, + typename TlsContext = std::monostate +> +class mqtt_client { +public: + using executor_type = typename StreamType::executor_type; +private: + using stream_type = StreamType; + using tls_context_type = TlsContext; + + static constexpr auto read_timeout = std::chrono::seconds(5); + + using client_service_type = detail::client_service< + stream_type, tls_context_type + >; + using clisvc_ptr = std::shared_ptr; + clisvc_ptr _svc_ptr; + +public: + + explicit mqtt_client( + const executor_type& ex, + const std::string& cnf, + tls_context_type tls_context = {} + ) : + _svc_ptr(std::make_shared( + ex, cnf, std::move(tls_context) + )) + {} + + template + requires (std::is_convertible_v) + explicit mqtt_client( + ExecutionContext& context, + const std::string& cnf, + TlsContext tls_context = {} + ) : + mqtt_client(context.get_executor(), cnf, std::move(tls_context)) + {} + + ~mqtt_client() { + cancel(); + } + + executor_type get_executor() const noexcept { + return _svc_ptr->get_executor(); + } + + decltype(auto) tls_context() + requires (!std::is_same_v) { + return _svc_ptr->tls_context(); + } + + void run() { + _svc_ptr->open_stream(); + detail::ping_op { _svc_ptr } + .perform(read_timeout - std::chrono::seconds(1)); + detail::read_message_op { _svc_ptr }.perform(); + detail::sentry_op { _svc_ptr }.perform(); + } + + void cancel() { + get_executor().execute([svc_ptr = _svc_ptr]() { + svc_ptr->cancel(); + }); + } + + mqtt_client& will(will will) { + _svc_ptr->will(std::move(will)); + return *this; + } + + mqtt_client& credentials( + std::string client_id, + std::string username = "", std::string password = "" + ) { + _svc_ptr->credentials( + std::move(client_id), + std::move(username), std::move(password) + ); + return *this; + } + + mqtt_client& brokers(std::string hosts, uint16_t default_port = 1883) { + _svc_ptr->brokers(std::move(hosts), default_port); + return *this; + } + + template + decltype(auto) async_publish( + std::string topic, std::string payload, + retain_e retain, const publish_props& props, + CompletionToken&& token + ) { + using Signature = detail::on_publish_signature; + + auto initiate = [] ( + auto handler, std::string topic, std::string payload, + retain_e retain, const publish_props& props, + const clisvc_ptr& svc_ptr + ) { + detail::publish_send_op< + client_service_type, decltype(handler), qos_type + > { svc_ptr, std::move(handler) } + .perform( + std::move(topic), std::move(payload), + retain, props + ); + }; + + return asio::async_initiate( + std::move(initiate), token, + std::move(topic), std::move(payload), retain, props, _svc_ptr + ); + } + + template + decltype(auto) async_subscribe( + const std::vector& topics, + const subscribe_props& props, + CompletionToken&& token + ) { + using Signature = void ( + error_code, std::vector, suback_props + ); + + auto initiate = [] ( + auto handler, const std::vector& topics, + const subscribe_props& props, const clisvc_ptr& impl + ) { + detail::subscribe_op { impl, std::move(handler) } + .perform(topics, props); + }; + + return asio::async_initiate( + std::move(initiate), token, topics, props, _svc_ptr + ); + } + + template + decltype(auto) async_subscribe( + const subscribe_topic& topic, const subscribe_props& props, + CompletionToken&& token + ) { + return async_subscribe( + std::vector { topic }, props, + std::forward(token) + ); + } + + template + decltype(auto) async_unsubscribe( + const std::vector& topics, const unsubscribe_props& props, + CompletionToken&& token + ) { + using Signature = void ( + error_code, std::vector, unsuback_props + ); + + auto initiate = []( + auto handler, + const std::vector& topics, + const unsubscribe_props& props, const clisvc_ptr& impl + ) { + detail::unsubscribe_op { impl, std::move(handler) } + .perform(topics, props); + }; + + return asio::async_initiate( + std::move(initiate), token, topics, props, _svc_ptr + ); + } + + template + decltype(auto) async_unsubscribe( + const std::string& topic, const unsubscribe_props& props, + CompletionToken&& token + ) { + return async_unsubscribe( + std::vector { topic }, props, + std::forward(token) + ); + } + + template + decltype(auto) async_receive(CompletionToken&& token) { + // Sig = void (error_code, std::string, std::string, publish_props) + return _svc_ptr->async_channel_receive( + std::forward(token) + ); + } + + template + decltype(auto) async_disconnect( + disconnect_rc_e reason_code, const disconnect_props& props, + CompletionToken&& token + ) { + return detail::async_disconnect( + reason_code, props, true, _svc_ptr, + std::forward(token) + ); + } + + template + decltype(auto) async_disconnect(CompletionToken&& token) { + return async_disconnect( + disconnect_rc_e::normal_disconnection, + disconnect_props {}, std::forward(token) + ); + } +}; + + +} // end namespace async_mqtt5 + +#endif // !ASYNC_MQTT5_MQTT_CLIENT_HPP diff --git a/include/async_mqtt5/property_types.hpp b/include/async_mqtt5/property_types.hpp new file mode 100644 index 0000000..e1c3eae --- /dev/null +++ b/include/async_mqtt5/property_types.hpp @@ -0,0 +1,167 @@ +#ifndef ASYNC_MQTT5_PROPERTY_TYPES_HPP +#define ASYNC_MQTT5_PROPERTY_TYPES_HPP + +#include +#include +#include +#include +#include + +namespace async_mqtt5::prop { + +constexpr std::integral_constant payload_format_indicator{}; +constexpr std::integral_constant message_expiry_interval{}; +constexpr std::integral_constant content_type{}; +constexpr std::integral_constant response_topic{}; +constexpr std::integral_constant correlation_data{}; +constexpr std::integral_constant subscription_identifier{}; +constexpr std::integral_constant session_expiry_interval{}; +constexpr std::integral_constant assigned_client_identifier{}; +constexpr std::integral_constant server_keep_alive{}; +constexpr std::integral_constant authentication_method{}; +constexpr std::integral_constant authentication_data{}; +constexpr std::integral_constant request_problem_information{}; +constexpr std::integral_constant will_delay_interval{}; +constexpr std::integral_constant request_response_information{}; +constexpr std::integral_constant response_information{}; +constexpr std::integral_constant server_reference{}; +constexpr std::integral_constant reason_string{}; +constexpr std::integral_constant receive_maximum{}; +constexpr std::integral_constant topic_alias_maximum{}; +constexpr std::integral_constant topic_alias{}; +constexpr std::integral_constant maximum_qos{}; +constexpr std::integral_constant retain_available{}; +constexpr std::integral_constant user_property{}; +constexpr std::integral_constant maximum_packet_size{}; +constexpr std::integral_constant wildcard_subscription_available{}; +constexpr std::integral_constant subscription_identifier_available{}; +constexpr std::integral_constant shared_subscription_available{}; + +template +struct property_traits; + +#define DEF_PROPERTY_TRAIT(Pname, Ptype) \ +template <> struct property_traits {\ + static constexpr std::string_view name = #Pname; \ + using type = Ptype;\ +}\ + + +DEF_PROPERTY_TRAIT(payload_format_indicator, std::optional); +DEF_PROPERTY_TRAIT(message_expiry_interval, std::optional); +DEF_PROPERTY_TRAIT(content_type, std::optional); +DEF_PROPERTY_TRAIT(response_topic, std::optional); +DEF_PROPERTY_TRAIT(correlation_data, std::optional); +DEF_PROPERTY_TRAIT(subscription_identifier, std::optional); +DEF_PROPERTY_TRAIT(session_expiry_interval, std::optional); +DEF_PROPERTY_TRAIT(assigned_client_identifier, std::optional); +DEF_PROPERTY_TRAIT(server_keep_alive, std::optional); +DEF_PROPERTY_TRAIT(authentication_method, std::optional); +DEF_PROPERTY_TRAIT(authentication_data, std::optional); +DEF_PROPERTY_TRAIT(request_problem_information, std::optional); +DEF_PROPERTY_TRAIT(will_delay_interval, std::optional); +DEF_PROPERTY_TRAIT(request_response_information, std::optional); +DEF_PROPERTY_TRAIT(response_information, std::optional); +DEF_PROPERTY_TRAIT(server_reference, std::optional); +DEF_PROPERTY_TRAIT(reason_string, std::optional); +DEF_PROPERTY_TRAIT(receive_maximum, std::optional); +DEF_PROPERTY_TRAIT(topic_alias_maximum, std::optional); +DEF_PROPERTY_TRAIT(topic_alias, std::optional); +DEF_PROPERTY_TRAIT(maximum_qos, std::optional); +DEF_PROPERTY_TRAIT(retain_available, std::optional); +DEF_PROPERTY_TRAIT(user_property, std::vector); +DEF_PROPERTY_TRAIT(maximum_packet_size, std::optional); +DEF_PROPERTY_TRAIT(wildcard_subscription_available, std::optional); +DEF_PROPERTY_TRAIT(subscription_identifier_available, std::optional); +DEF_PROPERTY_TRAIT(shared_subscription_available, std::optional); + +#undef DEF_PROPERTY_TRAIT + +template +using value_type_t = typename property_traits

::type; + +template +constexpr std::string_view name_v = property_traits

::name; + +template +class properties { + template + struct prop_type { + using key = decltype(p); + constexpr static std::string_view name = name_v

; + value_type_t

value; + }; + std::tuple...> _props; + +public: + + template + constexpr auto& operator[](std::integral_constant p) noexcept { + using Ptype = decltype(p); + return std::get>(_props).value; + } + + template + constexpr const auto& operator[](std::integral_constant p) const noexcept { + using Ptype = decltype(p); + return std::get>(_props).value; + } + + template + constexpr static bool is_apply_on_v = + std::conjunction_v&>...>; + + template + constexpr static bool is_nothrow_apply_on_v = + std::conjunction_v&>...>; + + template requires is_apply_on_v + constexpr bool apply_on(uint8_t property_id, Func&& func) noexcept(is_nothrow_apply_on_v) { + return std::apply([&func, property_id](auto&... props) { + auto pc = [&func, property_id](prop_type& px) { + if (prop.value == property_id) + std::invoke(func, px.value); + return prop.value != property_id; + }; + return (pc(props) && ...); + }, _props); + } + + template + constexpr static bool is_visitor_v = + std::conjunction_v&>...>; + + template + constexpr static bool is_nothrow_visitor_v = + std::conjunction_v&>...>; + + template requires is_visitor_v + constexpr bool visit(Func && func) const noexcept(is_nothrow_visitor_v) { + return std::apply( + [&func](const auto&... props) { + auto pc = [&func](const prop_type& px) { + return std::invoke(func, prop, px.value); + }; + return (pc(props) &&...); + }, + _props); + } + + template requires is_visitor_v + constexpr bool visit(Func&& func) noexcept(is_nothrow_visitor_v) { + return std::apply( + [&func](auto&... props) { + auto pc = [&func](prop_type& px) { + return std::invoke(func, prop, px.value); + }; + return (pc(props) && ...); + }, + _props); + } +}; + + + +} // end namespace async_mqtt5::prop + +#endif // !ASYNC_MQTT5_PROPERTY_TYPES_HPP diff --git a/include/async_mqtt5/types.hpp b/include/async_mqtt5/types.hpp new file mode 100644 index 0000000..bc6ed1c --- /dev/null +++ b/include/async_mqtt5/types.hpp @@ -0,0 +1,272 @@ +#ifndef ASYNC_MQTT5_TYPES_HPP +#define ASYNC_MQTT5_TYPES_HPP + +#include +#include +#include + +#include +#include + + +namespace async_mqtt5 { + +using error_code = boost::system::error_code; + +struct authority_path { + std::string host, port, path; +}; + +enum class qos_e : std::uint8_t { + at_most_once = 0b00, + at_least_once = 0b01, + exactly_once = 0b10 +}; + +enum class retain_e : std::uint8_t { + yes = 0b1, no = 0b0, +}; + +enum class dup_e : std::uint8_t { + yes = 0b1, no = 0b0, +}; + + +struct subscribe_options { + enum class no_local_e : std::uint8_t { + no = 0b0, + yes = 0b1, + }; + + enum class retain_as_published_e : std::uint8_t { + dont = 0b0, + retain = 0b1, + }; + + enum class retain_handling_e : std::uint8_t { + send = 0b00, + new_subscription_only = 0b01, + not_send = 0b10, + }; + + qos_e max_qos = qos_e::exactly_once; + no_local_e no_local = no_local_e::yes; + retain_as_published_e retain_as_published = retain_as_published_e::retain; + retain_handling_e retain_handling = retain_handling_e::new_subscription_only; +}; + +struct subscribe_topic { + std::string topic_filter; + subscribe_options sub_opts; +}; + +/* + +reason codes: + + 0x00 success + 0x01 success_qos_1 + 0x02 success_qos_2 + 0x04 disconnect_with_will_message + 0x10 no_matching_subscribers + 0x11 no_subscription_existed + 0x18 continue_authentication + 0x19 re_authenticate + 0x80 failure + 0x81 malformed_packet + 0x82 protocol_error + 0x83 implementation_specific_error + 0x84 unsupported_protocol_version + 0x85 client_identifier_not_valid + 0x86 bad_user_name_or_password + 0x87 not_authorized + 0x88 server_unavailable + 0x89 server_busy + 0x8a banned + 0x8b server_shutting_down + 0x8c bad_authentication_method + 0x8d keep_alive_timeout + 0x8e session_taken_over + 0x8f topic_filter_invalid + 0x90 topic_name_invalid + 0x91 packet_identifier_in_use + 0x92 packet_identifier_not_found + 0x93 receive_maximum_exceeded + 0x94 topic_alias_invalid + 0x95 packet_too_large + 0x95 packet_too_large + 0x96 message_rate_too_high + 0x97 quota_exceeded + 0x98 administrative_action + 0x99 payload_format_invalid + 0x9a retain_not_supported + 0x9b qos_not_supported + 0x9c use_another_server + 0x9d server_moved + 0x9e shared_subscriptions_not_supported + 0x9f connection_rate_exceeded + 0xa0 maximum_connect_time + 0xa1 subscription_identifiers_not_supported + 0xa2 wildcard_subscriptions_not_supported + +*/ + +class connect_props : public prop::properties< + prop::session_expiry_interval, + prop::receive_maximum, + prop::maximum_packet_size, + prop::topic_alias_maximum, + prop::request_response_information, + prop::request_problem_information, + prop::user_property, + prop::authentication_method, + prop::authentication_data +> {}; + +class connack_props : public prop::properties< + prop::session_expiry_interval, + prop::receive_maximum, + prop::maximum_qos, + prop::retain_available, + prop::maximum_packet_size, + prop::assigned_client_identifier, + prop::topic_alias_maximum, + prop::reason_string, + prop::user_property, + prop::wildcard_subscription_available, + prop::subscription_identifier_available, + prop::shared_subscription_available, + prop::server_keep_alive, + prop::response_information, + prop::server_reference, + prop::authentication_method, + prop::authentication_data +> {}; + +class publish_props : public prop::properties< + prop::payload_format_indicator, + prop::message_expiry_interval, + prop::content_type, + prop::response_topic, + prop::correlation_data, + prop::subscription_identifier, + prop::topic_alias, + prop::user_property +> {}; + +// puback, pubcomp +class puback_props : public prop::properties< + prop::reason_string, + prop::user_property +> {}; + +class pubcomp_props : public prop::properties< + prop::reason_string, + prop::user_property +> {}; + +class pubrec_props : public prop::properties< + prop::reason_string, + prop::user_property +> {}; + +class pubrel_props : public prop::properties< + prop::reason_string, + prop::user_property +> {}; + + +class subscribe_props : public prop::properties< + prop::subscription_identifier, + prop::user_property +> {}; + +class suback_props : public prop::properties< + prop::reason_string, + prop::user_property +> {}; + +class unsubscribe_props : public prop::properties< + prop::subscription_identifier +> {}; + +class unsuback_props : public prop::properties< + prop::reason_string, + prop::user_property +> {}; + +class disconnect_props : public prop::properties< + prop::session_expiry_interval, + prop::reason_string, + prop::user_property, + prop::server_reference +> {}; + +class auth_props : public prop::properties< + prop::authentication_method, + prop::authentication_data, + prop::reason_string, + prop::user_property +> {}; + + +class will_props : public prop::properties< + prop::will_delay_interval, + prop::payload_format_indicator, + prop::message_expiry_interval, + prop::content_type, + prop::response_topic, + prop::correlation_data, + prop::user_property +>{}; + +class will : public will_props { + std::string _topic; + std::string _message; + qos_e _qos; retain_e _retain; + +public: + will() = default; + + will( + std::string topic, std::string message, + qos_e qos = qos_e::at_most_once, retain_e retain = retain_e::no + ) : + _topic(std::move(topic)), _message(std::move(message)), + _qos(qos), _retain(retain) + {} + + will( + std::string topic, std::string message, + qos_e qos, retain_e retain, will_props props + ) : + will_props(std::move(props)), + _topic(std::move(topic)), _message(std::move(message)), + _qos(qos), _retain(retain) + {} + + // just to make sure that we don't accidentally make a copy + will(const will&) = delete; + will(will&&) noexcept = default; + + will& operator=(const will&) = delete; + will& operator=(will&&) noexcept = default; + + constexpr std::string_view topic() const { + return _topic; + } + constexpr std::string_view message() const { + return _message; + } + constexpr qos_e qos() const { + return _qos; + } + constexpr retain_e retain() const { + return _retain; + } +}; + + +} // end namespace async_mqtt5 + +#endif // !ASYNC_MQTT5_TYPES_HPP diff --git a/test/experimental/cancellation.cpp b/test/experimental/cancellation.cpp new file mode 100644 index 0000000..3976cd9 --- /dev/null +++ b/test/experimental/cancellation.cpp @@ -0,0 +1,155 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace asio = boost::asio; + +template +decltype(auto) tracking_executor(Handler&& handler) { + return asio::prefer( + asio::get_associated_executor(std::forward(handler)), + asio::execution::outstanding_work.tracked + ); +} + +template +using tracking_type = std::decay_t< + decltype(tracking_executor(std::declval())) +>; + +template +class async_op { + struct on_timer {}; + + std::decay_t _handler; + tracking_type _handler_ex; + asio::cancellation_state _cancel_state; + + // must be unique_ptr because move(timer) cancels previous op + std::unique_ptr _timer; +public: + template + async_op(const Executor& ex, Handler&& handler, const asio::cancellation_slot& cs) : + _handler(std::forward(handler)), + _handler_ex(tracking_executor(_handler)), + _cancel_state(cs), + _timer(std::make_unique(ex)) + {} + + async_op(async_op&&) noexcept = default; + async_op& operator=(async_op&&) noexcept = default; + + using executor_type = asio::steady_timer::executor_type; + executor_type get_executor() const noexcept { + return _timer->get_executor(); + } + + using allocator_type = asio::associated_allocator_t; + allocator_type get_allocator() const noexcept { + return asio::get_associated_allocator(_handler); + } + + using cancellation_slot_type = asio::cancellation_slot; + asio::cancellation_slot get_cancellation_slot() const noexcept { + return _cancel_state.slot(); + } + + void perform() { + _timer->expires_from_now(std::chrono::seconds(5)); + _timer->async_wait(asio::prepend(std::move(*this), on_timer {})); + } + + void operator()(on_timer, boost::system::error_code ec) { + if (ec == asio::error::operation_aborted) { + fmt::print(stderr, "Aborted {}\n", ec.message()); + return; + } + _cancel_state.slot().clear(); + + fmt::print(stderr, "Dispatching with error {}\n", ec.message()); + asio::dispatch(_handler_ex, [h = std::move(_handler), ec]() mutable { + std::move(h)(ec); + }); + } +}; + +class owner { + asio::cancellation_signal _cancel_signal; +public: + void cancel() { + _cancel_signal.emit(asio::cancellation_type::terminal); + _cancel_signal.slot().clear(); + } + ~owner() { + cancel(); + } + + template + decltype(auto) async_perform(asio::io_context& ioc, CompletionToken&& token) { + auto initiation = [this, &ioc](auto handler) { + auto slot = asio::get_associated_cancellation_slot(handler); + async_op( + ioc.get_executor(), std::move(handler), + slot.is_connected() ? slot : _cancel_signal.slot() + ).perform(); + }; + + return asio::async_initiate( + std::move(initiation), token + ); + } +}; + +void cancel_test(asio::io_context& ioc) { + asio::cancellation_signal cancel_signal; + + asio::thread_pool thp; + + { + owner b; + b.async_perform(ioc, + asio::bind_cancellation_slot( + cancel_signal.slot(), + asio::bind_executor(thp.get_executor(), + [](boost::system::error_code ec) { + fmt::print(stderr, "Finished with error {}\n", ec.message()); + } + ) + ) + ); + } + +/* + { + asio::steady_timer timer3(ioc); + timer3.expires_from_now(std::chrono::seconds(3)); + timer3.async_wait( + asio::bind_cancellation_slot( + cancel_signal.slot(), + [&] (boost::system::error_code) { + // cancel_signal.emit(asio::cancellation_type::terminal); + // b.cancel(); + } + ) + ); + } +*/ + asio::steady_timer timer2(ioc); + timer2.expires_from_now(std::chrono::seconds(1)); + timer2.async_wait([&] (boost::system::error_code) { + // cancel_signal.emit(asio::cancellation_type::terminal); + // b.cancel(); + }); + + ioc.run(); + thp.join(); +} + diff --git a/test/experimental/memory.cpp b/test/experimental/memory.cpp new file mode 100644 index 0000000..056ef9a --- /dev/null +++ b/test/experimental/memory.cpp @@ -0,0 +1,50 @@ +#include +#include + +#include "../../alloc/memory.h" +#include "../../alloc/string.h" +#include "../../alloc/vector.h" + +namespace asio = boost::asio; + +struct bx { + pma::string s1, s2; + + bx(std::string v1, std::string v2, const pma::alloc& alloc) : + s1(v1.begin(), v1.end(), alloc), + s2(v2.begin(), v2.end(), alloc) + {} +}; + +void test_memory() { + asio::recycling_allocator base_alloc; + pma::resource_adaptor> mem_res { base_alloc }; + auto alloc = pma::alloc(&mem_res); + + pma::string s1 { "abcdefgrthoasofjasfasf", alloc }; + pma::string s2 { alloc }; + s2 = s1; + + //TODO: the commented lines do not compile on Windows + //pma::vector v1 { { 'a', 'b'}, alloc }; + pma::vector v2 { alloc }; + //v2 = std::move(v1); + //pma::vector v3 = v2; + //pma::vector v4 = std::move(v3); + //v1.swap(v2); + + bx vbx { "ABCD", "EFGH", alloc }; + + fmt::print(stderr, "String = {}, is equal: {}\n", s2, + s1.get_allocator() == vbx.s2.get_allocator() + ); + + //fmt::print(stderr, "Vector allocators are equal: {}\n", + // v1.get_allocator() == v2.get_allocator() + //); + + std::allocator_traits::rebind_alloc char_alloc = + alloc; + +} + diff --git a/test/experimental/message_assembling.cpp b/test/experimental/message_assembling.cpp new file mode 100644 index 0000000..45201a6 --- /dev/null +++ b/test/experimental/message_assembling.cpp @@ -0,0 +1,199 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +namespace asio = boost::asio; + +namespace async_mqtt { + +using byte_iter = detail::byte_iter; + +class fake_stream { + asio::any_io_executor _ex; + std::string _data; + int _chunk_no = -1; + + std::string _read_buff; + detail::data_span _data_span; + +public: + fake_stream(asio::any_io_executor ex) : _ex(std::move(ex)) { + prepare_data(); + } + + using executor_type = asio::any_io_executor; + const executor_type& get_executor() const noexcept { return _ex; } + + template < + typename BufferType, + typename CompletionToken + > + auto async_read_some( + const BufferType& buffer, detail::duration wait_for, + CompletionToken&& token + ); + + template + decltype(auto) async_assemble( + Stream& stream, detail::duration wait_for, CompletionToken&& token + ); + +private: + void prepare_data(); + std::string_view next_frame(); +}; + +template < + typename BufferType, + typename CompletionToken +> +auto fake_stream::async_read_some( + const BufferType& buffer, detail::duration wait_for, + CompletionToken&& token +) { + auto read_op = [this] (auto handler, const BufferType& buffer) { + auto data = next_frame(); + size_t bytes_read = data.size(); + std::copy(data.begin(), data.end(), static_cast(buffer.data())); + + asio::post(get_executor(), [h = std::move(handler), bytes_read] () mutable { + h(error_code{}, bytes_read); + }); + }; + + return asio::async_initiate( + std::move(read_op), token, buffer + ); +} + +template +decltype(auto) fake_stream::async_assemble( + Stream& stream, detail::duration wait_for, CompletionToken&& token +) { + auto initiation = [this] (auto handler, Stream& stream, detail::duration wait_for) mutable { + detail::assemble_op ( + stream, std::move(handler), + _read_buff, _data_span + ).perform(wait_for, asio::transfer_at_least(0)); + }; + + return asio::async_initiate< + CompletionToken, void (error_code, uint8_t, byte_iter, byte_iter) + > ( + std::move(initiation), token, std::ref(stream), wait_for + ); +} + +void fake_stream::prepare_data() { + connack_props cap; + cap[prop::session_expiry_interval] = 60; + cap[prop::maximum_packet_size] = 16384; + cap[prop::wildcard_subscription_available] = true; + _data = encoders::encode_connack(true, 0x8A, cap); + + puback_props pap; + pap[prop::user_property].emplace_back("PUBACK user property"); + _data += encoders::encode_puback(42, 28, pap); +} + +std::string_view fake_stream::next_frame() { + ++_chunk_no; + if (_chunk_no == 0) + return { _data.begin(), _data.begin() + 2 }; + if (_chunk_no == 1) + return { _data.begin() + 2, _data.begin() + 13 }; + if (_chunk_no == 2) + return { _data.begin() + 13, _data.begin() + 23 }; + if (_chunk_no == 3) + return { _data.begin() + 23, _data.begin() + 35 }; + if (_chunk_no == 4) + return { _data.begin() + 35, _data.end() }; + return { _data.end(), _data.end() }; +} + +template +decltype(auto) async_assemble( + Stream& stream, detail::duration wait_for, + CompletionToken&& token +) { + return stream.async_assemble( + stream, wait_for, std::forward(token) + ); +} + + +void test_single(asio::io_context& ioc) { + using namespace std::chrono; + + fake_stream s(asio::make_strand(ioc)); + + auto on_message = [] ( + error_code ec, uint8_t control_code, byte_iter first, byte_iter last + ) { + fmt::print(stderr, "Error code: {}\n", ec.message()); + if (ec) return; + size_t remain_length = std::distance(first, last); + auto rv = decoders::decode_connack(control_code, remain_length, first); + const auto& [session_present, reason_code, cap] = *rv; + fmt::print(stderr, "Got CONNACK message, reason_code {}, session {}\n", reason_code, session_present); + fmt::print(stderr, "session_expiry_interval: {}\n", *cap[prop::session_expiry_interval]); + fmt::print(stderr, "maximum_packet_size: {}\n", *cap[prop::maximum_packet_size]); + fmt::print(stderr, "wildcard_subscription_available: {}\n", *cap[prop::wildcard_subscription_available]); + }; + + async_assemble(s, seconds(1), std::move(on_message)); + + ioc.run(); + ioc.restart(); +} + +asio::awaitable test_multiple_coro(asio::io_context& ioc) { + using namespace std::chrono; + + fake_stream s(asio::make_strand(ioc)); + + auto [ec1, cc1, first1, last1] = co_await async_assemble( + s, seconds(1), asio::use_nothrow_awaitable + ); + size_t remain_length1 = std::distance(first1, last1); + auto rv1 = decoders::decode_connack(cc1, remain_length1, first1); + if (rv1) + fmt::print(stderr, "CONNACK correctly decoded\n"); + + auto [ec2, cc2, first2, last2] = co_await async_assemble( + s, seconds(1), asio::use_nothrow_awaitable + ); + size_t remain_length2 = std::distance(first2, last2); + auto rv2 = decoders::decode_puback(cc2, remain_length2, first2); + + if (rv2) + fmt::print(stderr, "PUBACK correctly decoded\n"); +} + +void test_multiple(asio::io_context& ioc) { + co_spawn(ioc, test_multiple_coro(ioc), asio::detached); + ioc.run(); + ioc.restart(); +} + +} // end namespace async_mqtt + +void test_assembling(asio::io_context& ioc) { + using namespace std::chrono; + + async_mqtt::test_single(ioc); + async_mqtt::test_multiple(ioc); +} diff --git a/test/experimental/mutex.cpp b/test/experimental/mutex.cpp new file mode 100644 index 0000000..cd06f8c --- /dev/null +++ b/test/experimental/mutex.cpp @@ -0,0 +1,237 @@ +// #include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define ANKERL_NANOBENCH_IMPLEMENT +#include + +constexpr size_t nthreads = 4; +constexpr size_t per_job_count = 10'000 / nthreads; + +using namespace async_mqtt5::detail; + +void with_async_mutex(async_mutex& mutex, std::size_t* count) { + for (int c = 0; c < per_job_count; ++c) { + mutex.lock([&mutex, count](error_code) { + ++(*count); + mutex.unlock(); + }); + } +} + +void test_with_async_mutex(asio::any_io_executor e, async_mutex& mutex, size_t* count) { + asio::post(e, [&mutex, count]() { + with_async_mutex(mutex, count); + }); +} + +void with_old_async_mutex(async::mutex& mutex, std::size_t* count) { + for (int c = 0; c < per_job_count; ++c) { + mutex.lock([&mutex, count]() { + ++(*count); + mutex.unlock(); + }); + } +} + +void test_with_old_async_mutex(asio::any_io_executor e, async::mutex& mutex, size_t* count) { + asio::post(e, [&mutex, count]() { + with_old_async_mutex(mutex, count); + }); +} + + +struct std_mutex : public std::mutex { + std_mutex(asio::any_io_executor) : std::mutex() { } +}; + +void with_mutex(std_mutex& mutex, size_t* count) { + for (int c = 0; c < per_job_count; ++c) { + mutex.lock(); + ++(*count); + mutex.unlock(); + } +} + +void test_with_mutex(asio::any_io_executor e, std_mutex& mutex, size_t* count) { + asio::post(e, [&mutex, count]() { + with_mutex(mutex, count); + }); +} + +template +void test_with(Test func) { + asio::thread_pool pool(nthreads); + auto ex = pool.get_executor(); + auto count = std::make_unique(0); + Mutex mutex { ex }; + { + for (auto i = 0; i < nthreads; ++i) { + func(ex, mutex, count.get()); + } + } + pool.wait(); + if (*count != nthreads * per_job_count) + throw "greska!"; +} + +void test_cancellation() { + + asio::thread_pool tp; + asio::cancellation_signal cancel_signal; + + async_mutex mutex(tp.get_executor()); + asio::steady_timer timer(tp); + + auto op = [&](error_code ec) { + if (ec == asio::error::operation_aborted) { + mutex.unlock(); + return; + } + timer.expires_from_now(std::chrono::seconds(2)); + timer.async_wait([&] (boost::system::error_code) { + fmt::print(stderr, "Async-locked operation done\n"); + mutex.unlock(); + }); + }; + + auto cancellable_op = [&](error_code ec) { + fmt::print( + stderr, + "Cancellable async-locked operation finished with ec: {}\n", ec.message() + ); + if (ec == asio::error::operation_aborted) + return; + mutex.unlock(); + }; + + mutex.lock(std::move(op)); + + mutex.lock( + asio::bind_cancellation_slot( + cancel_signal.slot(), std::move(cancellable_op) + ) + ); + + asio::steady_timer timer2(tp); + timer2.expires_from_now(std::chrono::seconds(1)); + timer2.async_wait([&] (boost::system::error_code) { + cancel_signal.emit(asio::cancellation_type::terminal); + }); + + tp.wait(); +} + +void test_destructor() { + asio::thread_pool tp; + asio::cancellation_signal cancel_signal; + + { + async_mutex mutex(tp.get_executor()); + asio::steady_timer timer(tp); + + auto op = [&](error_code ec_mtx) { + if (ec_mtx == asio::error::operation_aborted) { + fmt::print( + stderr, + "Mutex operation cancelled error_code {}\n", + ec_mtx.message() + ); + mutex.unlock(); + return; + } + + timer.expires_from_now(std::chrono::seconds(2)); + timer.async_wait([&] (boost::system::error_code ec) { + if (ec == asio::error::operation_aborted) + return; + + fmt::print( + stderr, + "Async-locked operation done with error_code {}\n", + ec.message() + ); + mutex.unlock(); + }); + }; + + mutex.lock(std::move(op)); + } + + tp.wait(); +} + +void test_basics() { + asio::thread_pool tp; + + // { + asio::cancellation_signal cs; + async_mutex mutex(tp.get_executor()); + auto s1 = asio::make_strand(tp.get_executor()); + auto s2 = asio::make_strand(tp.get_executor()); + + mutex.lock(asio::bind_executor(s1, [&mutex, s1](boost::system::error_code ec) mutable { + fmt::print( + stderr, + "Scoped-locked operation (1) done with error_code {} ({})\n", + ec.message(), + s1.running_in_this_thread() + ); + if (ec != asio::error::operation_aborted) + mutex.unlock(); + })); + + mutex.lock( + asio::bind_cancellation_slot( + cs.slot(), + asio::bind_executor(s2, [s2](boost::system::error_code ec){ + fmt::print( + stderr, + "Scoped-locked operation (2) done with error_code {} ({})\n", + ec.message(), + s2.running_in_this_thread() + ); + }) + ) + ); + cs.emit(asio::cancellation_type_t::terminal); + cs.slot().clear(); + // } + + tp.wait(); +} + +void test_mutex() { + // test_basics(); + // return; + // test_destructor(); + // test_cancellation(); + // return; + + auto bench = ankerl::nanobench::Bench(); + bench.relative(true); + + bench.run("std::mutex", [] { + test_with(test_with_mutex); + }); + + bench.run("async_mutex", [] { + test_with(test_with_async_mutex); + }); + + bench.run("async::mutex", [] { + test_with(test_with_old_async_mutex); + }); +} diff --git a/test/experimental/uri_parse.cpp b/test/experimental/uri_parse.cpp new file mode 100644 index 0000000..4963086 --- /dev/null +++ b/test/experimental/uri_parse.cpp @@ -0,0 +1,46 @@ +#include + +template +static constexpr auto to_(T& arg) { + return [&](auto& ctx) { arg = boost::spirit::x3::_attr(ctx); }; +} + +template +static constexpr auto as_(Parser&& p){ + return boost::spirit::x3::rule{} = std::forward(p); +} + +void test_uri_parser(){ + struct authority_path { std::string host, port, path; }; + std::vector _servers; + + std::string brokers = "iot.fcluster.mireo.hr:1234, fc/nesto"; + std::string default_port = "8883"; + + namespace x3 = boost::spirit::x3; + + std::string host, port, path; + + // loosely based on RFC 3986 + auto unreserved_ = x3::char_("-a-zA-Z_0-9._~"); + auto digit_ = x3::char_("0-9"); + auto separator_ = x3::char_(','); + + auto host_ = as_(+unreserved_)[to_(host)]; + auto port_ = as_(':' >> +digit_)[to_(port)]; + auto path_ = as_('/' >> *unreserved_)[to_(path)]; + auto uri_ = *x3::omit[x3::space] >> (host_ >> *port_ >> *path_) >> + (*x3::omit[x3::space] >> x3::omit[separator_ | x3::eoi]); + + for (auto b = brokers.begin(); b != brokers.end(); ) { + host.clear(); port.clear(); path.clear(); + if (phrase_parse(b, brokers.end(), uri_, x3::eps(false))) { + _servers.push_back({ + std::move(host), port.empty() ? default_port : std::move(port), + std::move(path) + }); + } + else b = brokers.end(); + } +} + diff --git a/test/unit/include/test_common/delayed_op.hpp b/test/unit/include/test_common/delayed_op.hpp new file mode 100644 index 0000000..3819021 --- /dev/null +++ b/test/unit/include/test_common/delayed_op.hpp @@ -0,0 +1,111 @@ +#ifndef ASYNC_MQTT5_TEST_DELAYED_OP_HPP +#define ASYNC_MQTT5_TEST_DELAYED_OP_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace async_mqtt5::test { + +namespace asio = boost::asio; + +using error_code = boost::system::error_code; +using time_stamp = std::chrono::time_point; +using duration = time_stamp::duration; + +template +class delayed_op { + struct on_timer {}; + + std::unique_ptr _timer; + time_stamp::duration _delay; + asio::cancellation_slot _cancel_slot; + + std::tuple _args; + +public: + template + delayed_op( + const Executor& ex, time_stamp::duration delay, Args&& ...args + ) : + _timer(new asio::steady_timer(ex)), _delay(delay), + _args(std::move(args)...) + {} + + delayed_op(delayed_op&&) noexcept = default; + delayed_op(const delayed_op&) = delete; + + using executor_type = asio::steady_timer::executor_type; + executor_type get_executor() const noexcept { + return _timer->get_executor(); + } + + using allocator_type = asio::recycling_allocator; + allocator_type get_allocator() const noexcept { + return allocator_type {}; + } + + using cancellation_slot_type = asio::cancellation_slot; + asio::cancellation_slot get_cancellation_slot() const noexcept { + return _cancel_slot; + } + + template + void perform(CompletionHandler&& handler) { + _cancel_slot = asio::get_associated_cancellation_slot(handler); + + _timer->expires_from_now(_delay); + _timer->async_wait( + asio::prepend(std::move(*this), on_timer {}, std::move(handler)) + ); + } + + template + void operator()(on_timer, CompletionHandler&& h, error_code ec) { + get_cancellation_slot().clear(); + + auto bh = std::apply( + [h = std::move(h)](auto&&... args) mutable { + return asio::append(std::move(h), std::move(args)...); + }, + _args + ); + + asio::dispatch(asio::prepend(std::move(bh), ec)); + } +}; + +template +decltype(auto) async_delay( + asio::cancellation_slot cancel_slot, + delayed_op&& op, + CompletionToken&& token +) { + using Signature = void (error_code, std::remove_cvref_t...); + + auto initiation = []( + auto handler, asio::cancellation_slot cancel_slot, + delayed_op op + ) { + op.perform( + asio::bind_cancellation_slot(cancel_slot, std::move(handler)) + ); + }; + + return asio::async_initiate( + std::move(initiation), token, cancel_slot, std::move(op) + ); +} + + +} // end namespace async_mqtt5::test + +#endif // ASYNC_MQTT5_TEST_DELAYED_OP_HPP diff --git a/test/unit/include/test_common/message_exchange.hpp b/test/unit/include/test_common/message_exchange.hpp new file mode 100644 index 0000000..248d20f --- /dev/null +++ b/test/unit/include/test_common/message_exchange.hpp @@ -0,0 +1,254 @@ +#ifndef ASYNC_MQTT5_TEST_MESSAGE_EXCHANGE_HPP +#define ASYNC_MQTT5_TEST_MESSAGE_EXCHANGE_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "test_common/delayed_op.hpp" + +namespace async_mqtt5::test { + +namespace asio = boost::asio; + +using error_code = boost::system::error_code; +using time_stamp = std::chrono::time_point; +using duration = time_stamp::duration; + +class msg_exchange; +class broker_message; + +inline duration after(duration d) { return d; } + +using namespace std::chrono_literals; + +namespace detail { + +class stream_message { + error_code _ec; + duration _after { 0 }; + std::vector _content; + +public: + + template + stream_message(error_code ec, duration after, Args&& ...args) : + _ec(ec), _after(after) + { + (_content.insert(_content.end(), args.begin(), args.end()), ...); + } + + stream_message(const stream_message&) = delete; + stream_message(stream_message&&) = default; + + template + auto to_operation(const Executor& ex) { + return delayed_op> { + ex, _after, _ec, std::move(_content) + }; + } +}; + + +} // end namespace detail + + +class client_message { + msg_exchange* _owner; + + error_code _write_ec; + duration _complete_after { 0 }; + std::vector _expected_packets; + + std::vector _replies; + +public: + template + client_message(msg_exchange* owner, Args&&... args) : + _owner(owner), + _expected_packets({ std::forward(args)... }) + {} + + client_message(const client_message&) = delete; + client_message(client_message&&) = default; + + client_message& complete_with(error_code ec, duration af) { + _write_ec = ec; + _complete_after = af; + return *this; + } + + template + client_message& reply_with(Args&& ...args) { + // just to allow duration to be the last parameter + auto t = std::make_tuple(std::forward(args)...); + using Tuple = decltype(t); + + return[&](std::index_sequence) -> client_message& { + return reply_with_impl( + std::get -1>(t), + std::get(t)... + ); + }(std::make_index_sequence -1>{}); + } + + template + client_message& expect(Args&& ...args); + + template + broker_message& send(Args&& ...args); + + template + decltype(auto) write_completion(const Executor& ex) const { + return delayed_op(ex, _complete_after, _write_ec); + } + + template + decltype(auto) pop_reply_ops(const Executor& ex) { + std::vector>> ret; + std::transform( + _replies.begin(), _replies.end(), std::back_inserter(ret), + [&ex](auto& r) { return r.to_operation(ex); } + ); + _replies.clear(); + return ret; + } + +private: + + template + requires (std::is_same_v, std::string> && ...) + client_message& reply_with_impl(duration af, Args&& ...args) { + _replies.emplace_back( + error_code {}, af, std::forward(args)... + ); + return *this; + } + + client_message& reply_with_impl(duration af, error_code ec) { + _replies.emplace_back(ec, af); + return *this; + } +}; + +class broker_message { + msg_exchange* _owner; + detail::stream_message _message; + +public: + template + broker_message( + msg_exchange* owner, error_code ec, duration af, Args&&... args + ) : + _owner(owner), _message(ec, af, std::forward(args) ...) + {} + + broker_message(const broker_message&) = delete; + broker_message(broker_message&&) = default; + + template + client_message& expect(Args&& ...args); + + template + broker_message& send(Args&& ...args); + + template + decltype(auto) pop_send_op(const Executor& ex) { + return _message.to_operation(ex); + } +}; + + +class msg_exchange { + std::deque _to_broker; + std::vector _from_broker; + +public: + template + requires (std::is_same_v, std::string> && ...) + client_message& expect(Args&& ...args) { + _to_broker.emplace_back(this, std::forward(args)...); + return _to_broker.back(); + } + + template + broker_message& send(Args&& ...args) { + // just to allow duration to be the last parameter + auto t = std::make_tuple(std::forward(args)...); + using Tuple = decltype(t); + + return[&](std::index_sequence) -> broker_message& { + return send_impl( + std::get -1>(t), + std::get(t)... + ); + }(std::make_index_sequence -1>{}); + } + + std::optional pop_reply_action() { + if (_to_broker.empty()) + return std::nullopt; + + auto rv = std::move(_to_broker.front()); + _to_broker.pop_front(); + return rv; + } + + template + auto pop_broker_ops(const Executor& ex) { + std::vector>> ret; + std::transform( + _from_broker.begin(), _from_broker.end(), std::back_inserter(ret), + [&ex](auto& s) { return s.pop_send_op(ex); } + ); + _from_broker.clear(); + return ret; + } + +private: + + template + requires (std::is_same_v, std::string> && ...) + broker_message& send_impl(duration after, Args&& ...args) { + _from_broker.emplace_back( + this, error_code {}, after, std::forward(args)... + ); + return _from_broker.back(); + } + + broker_message& send_impl(duration after, error_code ec) { + _from_broker.emplace_back(this, ec, after); + return _from_broker.back(); + } +}; + +template +client_message& client_message::expect(Args&& ...args) { + return _owner->expect(std::forward(args)...); +} + +template +broker_message& client_message::send(Args&& ...args) { + return _owner->send(std::forward(args)...); +} + +template +client_message& broker_message::expect(Args&& ...args) { + return _owner->expect(std::forward(args)...); +} + +template +broker_message& broker_message::send(Args&& ...args) { + return _owner->send(std::forward(args)...); +} + + +} // end namespace async_mqtt5::test + +#endif // ASYNC_MQTT5_TEST_MESSAGE_EXCHANGE_HPP diff --git a/test/unit/include/test_common/packet_util.hpp b/test/unit/include/test_common/packet_util.hpp new file mode 100644 index 0000000..a086063 --- /dev/null +++ b/test/unit/include/test_common/packet_util.hpp @@ -0,0 +1,126 @@ +#ifndef ASYNC_MQTT5_TEST_PACKET_UTIL_HPP +#define ASYNC_MQTT5_TEST_PACKET_UTIL_HPP + +#include +#include + +#include +#include +#include + +namespace async_mqtt5::test { + + +inline qos_e extract_qos(uint8_t flags) { + auto byte = (flags & 0b0110) >> 1; + return qos_e(byte); +} + +inline control_code_e extract_code(uint8_t control_byte) { + using enum control_code_e; + + constexpr uint8_t mask = 0b11110000; + constexpr uint8_t publish_bits = 0b0011; + constexpr uint8_t special_mask = 0b00000010; + constexpr control_code_e codes_with_non_zero_end[] = { + pubrel, subscribe, unsubscribe + }; + + if ((control_byte >> 4) == publish_bits) + return publish; + if ((control_byte & mask) == control_byte) + return control_code_e(control_byte & mask); + + for (const auto& special_code : codes_with_non_zero_end) + if (control_byte == (uint8_t(special_code) | special_mask)) + return special_code; + + return no_packet; +} + + +inline std::string_view code_to_str(control_code_e code) { + using enum control_code_e; + + switch (code) { + case connect: return "CONNECT"; + case connack: return "CONNACK"; + case publish: return "PUBLISH"; + case puback: return "PUBACK"; + case pubrec: return "PUBREC"; + case pubrel: return "PUBREL"; + case pubcomp: return "PUBCOMP"; + case subscribe: return "SUBSCRIBE"; + case suback: return "SUBACK"; + case unsubscribe: return "UNSUBSCRIBE"; + case unsuback: return "UNSUBACK"; + case auth: return "AUTH"; + case disconnect: return "DISCONNECT"; + case pingreq: return "PINGREQ"; + case pingresp: return "PINGRESP"; + } + return "UNKNOWN"; +} + +inline std::string to_readable_packet( + std::string packet, error_code ec = {}, bool incoming = false +) { + using enum control_code_e; + + auto control_byte = uint8_t(*packet.data()); + auto code = extract_code(control_byte); + + if (code == no_packet) + return {}; + + std::ostringstream stream; + + if (incoming) + stream << "-> "; + + if (code == connect || code == connack || code == disconnect) { + stream << code_to_str(code) << (ec ? " ec: " + ec.message() : ""); + return stream.str(); + } + + auto begin = ++packet.cbegin(); + auto varlen = decoders::type_parse( + begin, packet.cend(), decoders::basic::varint_ + ); + + if (code == publish) { + auto publish = decoders::decode_publish( + control_byte, *varlen, begin + ); + auto& [topic, packet_id, flags, props, payload] = *publish; + stream << code_to_str(code); + stream << (packet_id ? " " + std::to_string(*packet_id) : ""); + return stream.str(); + } + + const auto packet_id = decoders::decode_packet_id(begin).value(); + stream << code_to_str(code) << " " << packet_id; + return stream.str(); +} + +template +std::vector to_packets(const ConstBufferSequence& buffers) { + std::vector content; + + for (const auto& buff : buffers) { + auto control_byte = *(const uint8_t*) buff.data(); + auto code = extract_code(control_byte); + + if (code == control_code_e::pingreq) + continue; + + content.push_back({ (const char*)buff.data(), buff.size() }); + } + + return content; +} + + +} // end namespace async_mqtt5::test + +#endif // ASYNC_MQTT5_TEST_PACKET_UTIL_HPP diff --git a/test/unit/include/test_common/protocol_logging.hpp b/test/unit/include/test_common/protocol_logging.hpp new file mode 100644 index 0000000..49d4602 --- /dev/null +++ b/test/unit/include/test_common/protocol_logging.hpp @@ -0,0 +1,21 @@ +#ifndef ASYNC_MQTT5_TEST_PROTOCOL_LOGGING_HPP +#define ASYNC_MQTT5_TEST_PROTOCOL_LOGGING_HPP + +#include + +namespace async_mqtt5::test { + +inline bool& logging_enabled() { + static bool enabled = false; + return enabled; +} + +inline void log(const std::string& message) { + if (logging_enabled()) + fprintf(stderr, "%s\n", message.c_str()); +} + + +} // end namespace async_mqtt5::test + +#endif // ASYNC_MQTT5_TEST_PROTOCOL_LOGGING_HPP diff --git a/test/unit/include/test_common/test_broker.hpp b/test/unit/include/test_common/test_broker.hpp new file mode 100644 index 0000000..2422e88 --- /dev/null +++ b/test/unit/include/test_common/test_broker.hpp @@ -0,0 +1,324 @@ +#ifndef ASYNC_MQTT5_TEST_TEST_BROKER_HPP +#define ASYNC_MQTT5_TEST_TEST_BROKER_HPP + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "test_common/protocol_logging.hpp" +#include "test_common/message_exchange.hpp" +#include "test_common/packet_util.hpp" + + +namespace async_mqtt5::test { + +namespace asio = boost::asio; +using error_code = boost::system::error_code; + +class pending_read { + void* _buffer_data { nullptr }; + size_t _buffer_size { 0 }; + asio::any_completion_handler _handler {}; + +public: + template + pending_read(const MutableBuffer& buffer, Handler&& handler) : + _buffer_data(asio::buffer_cast(buffer)), + _buffer_size(buffer.size()), + _handler(std::move(handler)) + {} + + pending_read() = default; + pending_read(pending_read&&) = default; + pending_read& operator=(pending_read&&) = default; + + size_t consume(const std::vector& data) { + size_t num_bytes = std::min(_buffer_size, data.size()); + std::memcpy(_buffer_data, data.data(), num_bytes); + return num_bytes; + } + + template + void complete(const Executor& ex, error_code ec, size_t bytes_read) { + if (empty()) + return; + if (ec || bytes_read || _buffer_size == 0) + asio::post(ex, asio::prepend(std::move(_handler), ec, bytes_read)); + } + + constexpr bool empty() const { + return !_handler; + } +}; + +class test_broker : public asio::execution_context::service { +public: + using executor_type = asio::any_io_executor; + using protocol_type = asio::ip::tcp; + using endpoint_type = asio::ip::tcp::endpoint; + + static inline asio::execution_context::id id {}; + +private: + using base = asio::execution_context::service; + + struct on_receive {}; + struct on_delayed_complete {}; + + struct broker_data { + error_code ec; + std::vector bytes; + }; + + executor_type _ex; + std::deque _broker_data; + pending_read _pending_read; + + msg_exchange _broker_side; + std::vector> _cancel_signals; + +public: + test_broker( + asio::execution_context& context, + asio::any_io_executor ex = {}, msg_exchange broker_side = {} + ) : + base(context), _ex(std::move(ex)), _broker_side(std::move(broker_side)) + { + launch_broker_ops(); + } + + test_broker(const test_broker&) = delete; + void operator=(const test_broker&) = delete; + + executor_type get_executor() const noexcept { + return _ex; + } + + void close_connection() { + _pending_read.complete( + get_executor(), asio::error::operation_aborted, 0 + ); + + for (auto& cs : _cancel_signals) + cs->emit(asio::cancellation_type::terminal); + + _broker_data.clear(); + } + + + template + decltype(auto) write_to_network( + const ConstBufferSequence& buffers, + WriteToken&& token + ) { + auto initiation = [this]( + auto handler, const ConstBufferSequence& buffers + ) { + auto reply_action = _broker_side.pop_reply_action(); + + size_t bytes_written = std::accumulate( + std::begin(buffers), std::end(buffers), size_t(0), + [](size_t a, const auto& b) { return a + b.size(); } + ); + + executor_type ex = get_executor(); + // TODO: validate + + auto complete_op = reply_action ? + reply_action->write_completion(ex) : + delayed_op(ex, 0ms, error_code {}); + + async_delay( + make_cancel_slot(), std::move(complete_op), + asio::prepend( + std::ref(*this), on_delayed_complete {}, + std::move(handler), bytes_written + ) + ); + + if (!reply_action.has_value()) + return; + + for (auto& op : reply_action->pop_reply_ops(ex)) + async_delay( + make_cancel_slot(), std::move(op), + asio::prepend(std::ref(*this), on_receive {}) + ); + }; + + return asio::async_initiate( + std::move(initiation), token, buffers + ); + } + + template + decltype(auto) read_from_network( + const MutableBuffer& buffer, ReadToken&& token + ) { + auto initiation = [this]( + auto handler, const MutableBuffer& buffer + ) { + _pending_read = pending_read(buffer, std::move(handler)); + complete_read(); + }; + + return asio::async_initiate( + std::move(initiation), token, buffer + ); + } + + + void operator()( + on_receive, error_code delay_ec, + error_code ec, std::vector bytes + ) { + remove_cancel_signal(); + + if (delay_ec) // asio::operation_aborted + return; + + _broker_data.push_back({ ec, std::move(bytes) }); + complete_read(); + } + + template + void operator()( + on_delayed_complete, Handler handler, size_t bytes, + error_code delay_ec, error_code ec + ) { + remove_cancel_signal(); + + if (delay_ec) { // asio::operation_aborted + ec = delay_ec; + bytes = 0; + } + + asio::dispatch(asio::prepend(std::move(handler), ec, bytes)); + } + +private: + + void shutdown() override { } + + void launch_broker_ops() { + for (auto& op: _broker_side.pop_broker_ops(get_executor())) { + async_delay( + asio::cancellation_slot {}, + std::move(op), + asio::prepend(std::ref(*this), on_receive {}) + ); + } + } + + void complete_read() { + if (_pending_read.empty()) + return; + + error_code ec = {}; + size_t bytes_read = 0; + + if (!_broker_data.empty()) { + auto& [read_ec, bytes] = _broker_data.front(); + ec = read_ec; + bytes_read = _pending_read.consume(bytes); + + if (bytes_read == bytes.size()) + _broker_data.pop_front(); + else + bytes.erase(bytes.begin(), bytes.begin() + bytes_read); + } + + _pending_read.complete(get_executor(), ec, bytes_read); + } + + asio::cancellation_slot make_cancel_slot() { + _cancel_signals.push_back( + std::make_unique() + ); + return _cancel_signals.back()->slot(); + } + + void remove_cancel_signal() { + _cancel_signals.erase( + std::remove_if( + _cancel_signals.begin(), _cancel_signals.end(), + [](auto& sig_ptr) { return !sig_ptr->slot().has_handler(); } + ), + _cancel_signals.end() + ); + } +}; + +} // end namespace async_mqtt5::test + + + +// Funs temporarily moved out of network service +// +//void process_packet(const std::string& packet) { +// using enum control_code_e; +// +// auto code = extract_code(uint8_t(*packet.data())); +// if (code == connack) +// determine_network_properties(packet); +// else if (code == puback || code == pubcomp) +// _num_outgoing_publishes--; +//} +// +//void determine_network_properties(const std::string& connack) { +// auto begin = connack.cbegin() + 1 /* fixed header */; +// auto _ = decoders::type_parse(begin, connack.cend(), decoders::basic::varint_); +// auto rv = decoders::decode_connack(connack.size(), begin); +// const auto& [session_present, reason_code, ca_props] = *rv; +// +// if (ca_props[prop::receive_maximum]) +// _max_receive = *ca_props[prop::receive_maximum]; +// else +// _max_receive = MAX_LIMIT; +//} +// +//void count_outgoing_publishes( +// const std::vector& packets +//) { +// for (const auto& packet : packets) { +// auto code = extract_code(uint8_t(*packet.data())); +// if (code == control_code_e::publish) { +// auto flags = *packet.data() & 0b00001111; +// auto qos = extract_qos(flags); +// +// if (qos != qos_e::at_most_once) +// _num_outgoing_publishes++; +// +// BOOST_ASIO_CHECK_MESSAGE( +// _num_outgoing_publishes <= _max_receive, +// "There are more outgoing PUBLISH packets than\ +// it is allowed by the Maxmimum Receive Limit!" +// ); +// } +// } +//} + + +// write_to_network stuff + +//auto packets = to_packets(buffers); +//count_outgoing_publishes(packets); +//// TODO: this is just for debug right now +//if (!write_ec) +// for (const auto& packet : packets) +// test::log(to_readable_packet(packet, write_ec, false)); + +#endif // ASYNC_MQTT5_TEST_TEST_BROKER_HPP diff --git a/test/unit/include/test_common/test_service.hpp b/test/unit/include/test_common/test_service.hpp new file mode 100644 index 0000000..ec697c8 --- /dev/null +++ b/test/unit/include/test_common/test_service.hpp @@ -0,0 +1,49 @@ +#ifndef ASYNC_MQTT5_TEST_TEST_SERVICE_HPP +#define ASYNC_MQTT5_TEST_TEST_SERVICE_HPP + +#include + +#include +#include +#include + +#include + +namespace async_mqtt5::test { + +template < + typename StreamType, + typename TlsContext = std::monostate +> +class test_service : public detail::client_service { + using error_code = boost::system::error_code; + using base = detail::client_service; + + asio::any_io_executor _ex; +public: + test_service(const asio::any_io_executor ex) + : base(ex, {}), _ex(ex) + {} + + template + decltype(auto) async_send( + const BufferType&, uint32_t, unsigned, + CompletionToken&& token + ) { + auto initiation = [this](auto handler) { + auto ex = boost::asio::get_associated_executor(handler, _ex); + boost::asio::post(ex, + boost::asio::prepend(std::move(handler), error_code {}) + ); + }; + + return boost::asio::async_initiate< + CompletionToken, void (error_code) + > (std::move(initiation), token); + } +}; + + +} // end namespace async_mqtt5::test + +#endif // ASYNC_MQTT5_TEST_TEST_SERVICE_HPP diff --git a/test/unit/include/test_common/test_stream.hpp b/test/unit/include/test_common/test_stream.hpp new file mode 100644 index 0000000..4373157 --- /dev/null +++ b/test/unit/include/test_common/test_stream.hpp @@ -0,0 +1,337 @@ +#ifndef ASYNC_MQTT5_TEST_TEST_STREAM_HPP +#define ASYNC_MQTT5_TEST_TEST_STREAM_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include "test_common/test_broker.hpp" + +namespace async_mqtt5::test { + +namespace asio = boost::asio; +namespace asioex = asio::experimental; + +using error_code = boost::system::error_code; +using time_stamp = std::chrono::time_point; +using duration = time_stamp::duration; + +namespace detail { + +class test_stream_impl { +public: + using executor_type = test_broker::executor_type; + using protocol_type = test_broker::protocol_type; + using endpoint_type = test_broker::endpoint_type; + +private: + executor_type _ex; + test_broker* _test_broker { nullptr }; + endpoint_type _remote_ep; + + template + friend class read_op; + + template + friend class write_op; + +public: + test_stream_impl(executor_type ex) : _ex(std::move(ex)) {} + + executor_type get_executor() const noexcept { + return _ex; + } + + void open(const protocol_type&, error_code& ec) { + ec = {}; + _test_broker = &asio::use_service(_ex.context()); + } + + void close(error_code& ec) { + disconnect(); + ec = {}; + _test_broker = nullptr; + } + + void shutdown(asio::ip::tcp::socket::shutdown_type, error_code& ec) { + ec = {}; + } + + void connect(const endpoint_type& ep, error_code& ec) { + ec = {}; + _remote_ep = ep; + } + + void disconnect() { + _remote_ep = {}; + if (_test_broker) + _test_broker->close_connection(); + } + + endpoint_type remote_endpoint(error_code& ec) { + if (_remote_ep == endpoint_type {}) + ec = asio::error::not_connected; + else + ec = {}; + return _remote_ep; + } + + bool is_open() const { + return _test_broker != nullptr; + } + + bool is_connected() const { + return _remote_ep != endpoint_type {}; + } +}; + + + +template +class read_op { + struct on_read {}; + std::shared_ptr _stream_impl; + std::decay_t _handler; + +public: + read_op( + std::shared_ptr stream_impl, Handler handler + ) : + _stream_impl(std::move(stream_impl)), + _handler(std::move(handler)) + {} + + read_op(read_op&&) noexcept = default; + read_op(const read_op&) = delete; + + using executor_type = test_stream_impl::executor_type; + executor_type get_executor() const noexcept { + return _stream_impl->get_executor(); + } + + using allocator_type = asio::recycling_allocator; + allocator_type get_allocator() const noexcept { + return allocator_type {}; + } + + template + void perform(const BufferType& buffer) { + if (!_stream_impl->is_open() || !_stream_impl->is_connected()) + return complete_post(asio::error::not_connected, 0); + + _stream_impl->_test_broker->read_from_network( + buffer, + asio::prepend(std::move(*this), on_read {}) + ); + } + + void operator()(on_read, error_code ec, size_t bytes_read) { + if (ec) + _stream_impl->disconnect(); + complete(ec, bytes_read); + } + +private: + void complete_post(error_code ec, size_t bytes_read) { + asio::post( + get_executor(), + asio::prepend(std::move(_handler), ec, bytes_read) + ); + } + + void complete(error_code ec, size_t bytes_read) { + asio::dispatch( + get_executor(), + asio::prepend(std::move(_handler), ec, bytes_read) + ); + } +}; + +template +class write_op { + struct on_write {}; + + std::shared_ptr _stream_impl; + std::decay_t _handler; + +public: + write_op( + std::shared_ptr stream_impl, Handler handler + ) : + _stream_impl(std::move(stream_impl)), + _handler(std::move(handler)) + {} + + write_op(write_op&&) noexcept = default; + write_op(const write_op&) = delete; + + using executor_type = test_stream_impl::executor_type; + executor_type get_executor() const noexcept { + return _stream_impl->get_executor(); + } + + using allocator_type = asio::recycling_allocator; + allocator_type get_allocator() const noexcept { + return allocator_type {}; + } + + template + void perform(const BufferType& buffers) { + if (!_stream_impl->is_open() || !_stream_impl->is_connected()) + return complete_post(asio::error::not_connected, 0); + + _stream_impl->_test_broker->write_to_network( + buffers, + asio::prepend(std::move(*this), on_write {}) + ); + } + + void operator()(on_write, error_code ec, size_t bytes_written) { + if (ec) + _stream_impl->disconnect(); + complete(ec, bytes_written); + } + +private: + void complete_post(error_code ec, size_t bytes_written) { + asio::post( + get_executor(), + asio::prepend(std::move(_handler), ec, bytes_written) + ); + } + + void complete(error_code ec, size_t bytes_written) { + asio::dispatch( + get_executor(), + asio::prepend(std::move(_handler), ec, bytes_written) + ); + } +}; + +} // end namespace detail + +class test_stream { +public: + using executor_type = test_broker::executor_type; + using protocol_type = test_broker::protocol_type; + using endpoint_type = test_broker::endpoint_type; + +private: + std::shared_ptr _impl; + +public: + test_stream(executor_type ex) : + _impl(std::make_shared(std::move(ex))) + {} + + test_stream(const test_stream&) = delete; + + ~test_stream() { + error_code ec; + close(ec); // cancel() would be more appropriate + } + + executor_type get_executor() const noexcept { + return _impl->get_executor(); + } + + void open(const protocol_type& p, error_code& ec) { + _impl->open(p, ec); + } + + void close(error_code& ec) { + _impl->close(ec); + } + + void connect(const endpoint_type& ep, error_code& ec) { + _impl->connect(ep, ec); + } + + void disconnect() { + _impl->disconnect(); + } + + bool is_open() const { + return _impl->is_open(); + } + + bool is_connected() const { + return _impl->is_connected(); + } + + void shutdown(asio::ip::tcp::socket::shutdown_type st, error_code& ec) { + return _impl->shutdown(st, ec); + } + + endpoint_type remote_endpoint(error_code& ec) { + return _impl->remote_endpoint(ec); + } + + template + void set_option(const SettableSocketOption&, error_code&) {} + + template + decltype(auto) async_connect( + const endpoint_type& ep, ConnectToken&& token + ) { + + auto initiation = [this](auto handler, const endpoint_type& ep) { + error_code ec; + open(asio::ip::tcp::v4(), ec); + + if (!ec) + connect(ep, ec); + + asio::post(get_executor(), asio::prepend(std::move(handler), ec)); + }; + + return async_initiate( + std::move(initiation), token, ep + ); + } + + template + decltype(auto) async_write_some( + const ConstBufferSequence& buffers, WriteToken&& token + ) { + using Signature = void (error_code, size_t); + + auto initiation = [this]( + auto handler, const ConstBufferSequence& buffers + ) { + detail::write_op { _impl, std::move(handler) }.perform(buffers); + }; + + return asio::async_initiate( + std::move(initiation), token, buffers + ); + } + + template + decltype(auto) async_read_some( + const MutableBufferSequence& buffers, + ReadToken&& token + ) { + using Signature = void (error_code, size_t); + + auto initiation = [this]( + auto handler, const MutableBufferSequence& buffers + ) { + detail::read_op { _impl, std::move(handler) }.perform(buffers); + }; + + return asio::async_initiate( + std::move(initiation), token, buffers + ); + } + +}; + + +} // end namespace async_mqtt5::test + +#endif // ASYNC_MQTT5_TEST_TEST_STREAM_HPP diff --git a/test/unit/src/run_tests.cpp b/test/unit/src/run_tests.cpp new file mode 100644 index 0000000..215a4ff --- /dev/null +++ b/test/unit/src/run_tests.cpp @@ -0,0 +1,21 @@ +#include +#include + +boost::unit_test::test_suite* init_tests( + int /*argc*/, char* /*argv*/[] +) { + async_mqtt5::test::logging_enabled() = true; + return nullptr; +} + +int main(int argc, char* argv[]) { + return boost::unit_test::unit_test_main(&init_tests, argc, argv); +} + +/* +* usage: ./mqtt-test [boost test --arg=val]* +* example: ./mqtt-test --log_level=test_suite +* +* all boost test parameters can be found here: +* https://www.boost.org/doc/libs/1_82_0/libs/test/doc/html/boost_test/runtime_config/summary.html +*/ diff --git a/test/unit/test/client_broker.cpp b/test/unit/test/client_broker.cpp new file mode 100644 index 0000000..53c4d68 --- /dev/null +++ b/test/unit/test/client_broker.cpp @@ -0,0 +1,576 @@ +#include + +#include +#include + +#include +#include + +#include "test_common/test_stream.hpp" +#include "test_common/message_exchange.hpp" + +#include + +using namespace async_mqtt5; + +BOOST_AUTO_TEST_SUITE(framework, *boost::unit_test::disabled()) + +BOOST_AUTO_TEST_CASE(publish_qos_0) { + using test::after; + using std::chrono_literals::operator ""ms; + + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + + // packets + auto connect = encoders::encode_connect( + "", std::nullopt, std::nullopt, 10, false, {}, std::nullopt + ); + auto connack = encoders::encode_connack( + false, reason_codes::success.value(), {} + ); + auto publish_1 = encoders::encode_publish( + 65535, "t", "p_1", qos_e::at_most_once, retain_e::no, dup_e::no, {} + ); + + test::msg_exchange broker_side; + error_code success {}; + + broker_side + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(20ms)) + .expect(publish_1); + + asio::io_context ioc; + auto executor = ioc.get_executor(); + asio::make_service(ioc, executor, std::move(broker_side)); + + using client_type = mqtt_client; + client_type c(executor, ""); + c.brokers("127.0.0.1") + .run(); + + c.async_publish( + "t", "p_1", retain_e::no, publish_props{}, + [&](error_code ec) { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + ++handlers_called; + } + ); + + asio::steady_timer timer(c.get_executor()); + timer.expires_after(std::chrono::seconds(1)); + timer.async_wait([&](auto) { c.cancel(); }); + + + ioc.run(); + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + + +BOOST_AUTO_TEST_CASE(two_publishes_qos_1_with_fail_on_write) { + using test::after; + using std::chrono_literals::operator ""ms; + + constexpr int expected_handlers_called = 2; + int handlers_called = 0; + + // packets + auto connect = encoders::encode_connect( + "", std::nullopt, std::nullopt, 10, false, {}, std::nullopt + ); + auto connack = encoders::encode_connack( + false, reason_codes::success.value(), {} + ); + auto publish_1 = encoders::encode_publish( + 65535, "t", "p_1", qos_e::at_least_once, retain_e::no, dup_e::no, {} + ); + auto puback_1 = encoders::encode_puback( + 65535, reason_codes::success.value(), {} + ); + auto publish_2 = encoders::encode_publish( + 65534, "t", "p_2", qos_e::at_least_once, retain_e::no, dup_e::no, {} + ); + auto puback_2 = encoders::encode_puback( + 65534, reason_codes::success.value(), {} + ); + + test::msg_exchange broker_side; + error_code success {}; + error_code fail = asio::error::not_connected; + + broker_side + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(10ms)) + .expect(publish_1) + .complete_with(fail, after(10ms)) + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(10ms)) + .expect(publish_1, publish_2) + .complete_with(success, after(10ms)) + .reply_with(puback_1, puback_2, after(20ms)); + + asio::io_context ioc; + auto executor = ioc.get_executor(); + asio::make_service(ioc, executor, std::move(broker_side)); + + using client_type = mqtt_client; + client_type c(executor, ""); + c.brokers("127.0.0.1") + .run(); + + c.async_publish( + "t", "p_1", retain_e::no, publish_props{}, + [&](error_code ec, reason_code rc, auto) { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_MESSAGE(!rc, rc.message()); + ++handlers_called; + } + ); + + c.async_publish( + "t", "p_2", retain_e::no, publish_props{}, + [&](error_code ec, reason_code rc, auto) { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_MESSAGE(!rc, rc.message()); + ++handlers_called; + } + ); + + asio::steady_timer timer(c.get_executor()); + timer.expires_after(std::chrono::seconds(6)); + timer.async_wait([&](auto) { c.cancel(); }); + + ioc.run(); + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + +BOOST_AUTO_TEST_CASE(receive_publish_qos_2) { + using test::after; + using std::chrono_literals::operator ""ms; + + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + std::string topic = "topic"; + std::string payload = "payload"; + + // packets + auto connect = encoders::encode_connect( + "", std::nullopt, std::nullopt, 10, false, {}, std::nullopt + ); + auto connack = encoders::encode_connack( + false, reason_codes::success.value(), {} + ); + auto publish = encoders::encode_publish( + 65535, topic, payload, qos_e::exactly_once, retain_e::no, dup_e::no, {} + ); + auto pubrec = encoders::encode_pubrec( + 65535, reason_codes::success.value(), {} + ); + auto pubrel = encoders::encode_pubrel( + 65535, reason_codes::success.value(), {} + ); + auto pubcomp = encoders::encode_pubcomp( + 65535, reason_codes::success.value(), {} + ); + + test::msg_exchange broker_side; + error_code success {}; + + broker_side + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(15ms)) + .send(publish, after(300ms)) + .expect(pubrec) + .complete_with(success, after(10ms)) + .reply_with(pubrel, after(15ms)) + .expect(pubcomp) + .complete_with(success, after(5ms)); + + + asio::io_context ioc; + auto executor = ioc.get_executor(); + asio::make_service(ioc, executor, std::move(broker_side)); + + using client_type = mqtt_client; + client_type c(executor, ""); + c.brokers("127.0.0.1") + .run(); + + c.async_receive( + [&](error_code ec, std::string rec_topic, std::string rec_payload, publish_props) + { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_EQUAL(topic, rec_topic); + BOOST_CHECK_EQUAL(payload, rec_payload); + ++handlers_called; + } + ); + + asio::steady_timer timer(c.get_executor()); + timer.expires_after(std::chrono::seconds(1)); + timer.async_wait([&](auto) { c.cancel(); }); + + ioc.run(); + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + +BOOST_AUTO_TEST_CASE(send_publish_qos_2_with_fail_on_read) { + using test::after; + using std::chrono_literals::operator ""ms; + + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + + // packets + auto connect = encoders::encode_connect( + "", std::nullopt, std::nullopt, 10, false, {}, std::nullopt + ); + auto connack = encoders::encode_connack( + false, reason_codes::success.value(), {} + ); + auto publish = encoders::encode_publish( + 65535, "t_1", "p_1", qos_e::exactly_once, retain_e::no, dup_e::no, {} + ); + auto pubrec = encoders::encode_pubrec( + 65535, reason_codes::success.value(), {} + ); + auto pubrel = encoders::encode_pubrel( + 65535, reason_codes::success.value(), {} + ); + auto pubcomp = encoders::encode_pubcomp( + 65535, reason_codes::success.value(), {} + ); + + test::msg_exchange broker_side; + error_code success {}; + error_code fail = asio::error::not_connected; + + broker_side + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(20ms)) + .expect(publish) + .complete_with(success, after(10ms)) + .reply_with(pubrec, after(25ms)) + .expect(pubrel) + .complete_with(success, after(10ms)) + .reply_with(fail, after(10ms)) + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(20ms)) + .expect(pubrel) + .complete_with(success, after(10ms)) + .reply_with(pubcomp, after(20ms)); + + asio::io_context ioc; + auto executor = ioc.get_executor(); + asio::make_service(ioc, executor, std::move(broker_side)); + + using client_type = mqtt_client; + client_type c(executor, ""); + + c.brokers("127.0.0.1") + .run(); + + c.async_publish( + "t_1", "p_1", retain_e::no, publish_props{}, + [&](error_code ec, reason_code rc, auto) { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_MESSAGE(!rc, rc.message()); + ++handlers_called; + } + ); + + asio::steady_timer timer(c.get_executor()); + timer.expires_after(std::chrono::seconds(7)); + timer.async_wait([&](auto) { c.cancel(); }); + + ioc.run(); + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + +BOOST_AUTO_TEST_CASE(test_ordering_after_reconnect) { + using test::after; + using std::chrono_literals::operator ""ms; + + constexpr int expected_handlers_called = 2; + int handlers_called = 0; + + // packets + auto connect = encoders::encode_connect( + "", std::nullopt, std::nullopt, 10, false, {}, std::nullopt + ); + auto connack = encoders::encode_connack( + false, reason_codes::success.value(), {} + ); + auto publish_1 = encoders::encode_publish( + 65535, "t_1", "p_1", qos_e::at_least_once, retain_e::no, dup_e::no, {} + ); + auto publish_1_dup = encoders::encode_publish( + 65535, "t_1", "p_1", qos_e::at_least_once, retain_e::no, dup_e::yes, {} + ); + auto puback = encoders::encode_puback( + 65535, reason_codes::success.value(), {} + ); + auto publish_2 = encoders::encode_publish( + 65534, "t_2", "p_2", qos_e::exactly_once, retain_e::no, dup_e::no, {} + ); + auto pubrec = encoders::encode_pubrec( + 65534, reason_codes::success.value(), {} + ); + auto pubrel = encoders::encode_pubrel( + 65534, reason_codes::success.value(), {} + ); + auto pubcomp = encoders::encode_pubcomp( + 65534, reason_codes::success.value(), {} + ); + + test::msg_exchange broker_side; + error_code success {}; + error_code fail = asio::error::not_connected; + + broker_side + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(20ms)) + .expect(publish_1, publish_2) + .complete_with(success, after(10ms)) + .reply_with(pubrec, after(20ms)) + .expect(pubrel) + .complete_with(fail, after(10ms)) + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(15ms)) + .expect(pubrel, publish_1_dup) + .complete_with(success, after(10ms)) + .reply_with(pubcomp, puback, after(20ms)); + + + asio::io_context ioc; + auto executor = ioc.get_executor(); + asio::make_service(ioc, executor, std::move(broker_side)); + + using client_type = mqtt_client; + client_type c(executor, ""); + c.brokers("127.0.0.1") + .run(); + + c.async_publish( + "t_1", "p_1", retain_e::no, publish_props{}, + [&](error_code ec, reason_code rc, auto) { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_MESSAGE(!rc, rc.message()); + ++handlers_called; + } + ); + + c.async_publish( + "t_2", "p_2", retain_e::no, publish_props{}, + [&](error_code ec, reason_code rc, auto) { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_MESSAGE(!rc, rc.message()); + ++handlers_called; + } + ); + + asio::steady_timer timer(c.get_executor()); + timer.expires_after(std::chrono::seconds(7)); + timer.async_wait([&](auto) { c.cancel(); }); + + ioc.run(); + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + +BOOST_AUTO_TEST_CASE(throttling) { + using test::after; + using std::chrono_literals::operator ""ms; + + constexpr int expected_handlers_called = 3; + int handlers_called = 0; + + connack_props props; + props[prop::receive_maximum] = 1; + + //packets + auto connect = encoders::encode_connect( + "", std::nullopt, std::nullopt, 10, false, {}, std::nullopt + ); + auto connack = encoders::encode_connack( + false, reason_codes::success.value(), props + ); + auto publish_1 = encoders::encode_publish( + 65535, "t_1", "p_1", qos_e::at_least_once, retain_e::no, dup_e::no, {} + ); + auto publish_2 = encoders::encode_publish( + 65534, "t_1", "p_2", qos_e::at_least_once, retain_e::no, dup_e::no, {} + ); + auto publish_3 = encoders::encode_publish( + 65533, "t_1", "p_3", qos_e::at_least_once, retain_e::no, dup_e::no, {} + ); + auto puback_1 = encoders::encode_puback( + 65535, reason_codes::success.value(), {} + ); + auto puback_2 = encoders::encode_puback( + 65534, reason_codes::success.value(), {} + ); + auto puback_3 = encoders::encode_puback( + 65533, reason_codes::success.value(), {} + ); + + test::msg_exchange broker_side; + error_code success {}; + error_code fail = asio::error::not_connected; + + broker_side + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(15ms)) + .expect(publish_1) + .complete_with(success, after(10ms)) + .reply_with(puback_1, after(15ms)) + .expect(publish_2) + .complete_with(success, after(10ms)) + .reply_with(puback_2, after(15ms)) + .expect(publish_3) + .complete_with(success, after(10ms)) + .reply_with(puback_3, after(15ms)); + + asio::io_context ioc; + auto executor = ioc.get_executor(); + asio::make_service(ioc, executor, std::move(broker_side)); + + using client_type = mqtt_client; + client_type c(executor, ""); + c.brokers("127.0.0.1") + .run(); + + c.async_publish( + "t_1", "p_1", retain_e::no, publish_props{}, + [&](error_code ec, reason_code rc, auto) { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_MESSAGE(!rc, rc.message()); + BOOST_CHECK_EQUAL(handlers_called, 0); + ++handlers_called; + } + ); + + + c.async_publish( + "t_1", "p_2", retain_e::no, publish_props{}, + [&](error_code ec, reason_code rc, auto) { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_MESSAGE(!rc, rc.message()); + BOOST_CHECK_EQUAL(handlers_called, 1); + ++handlers_called; + } + ); + + c.async_publish( + "t_1", "p_3", retain_e::no, publish_props{}, + [&](error_code ec, reason_code rc, auto) { + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_MESSAGE(!rc, rc.message()); + BOOST_CHECK_EQUAL(handlers_called, 2); + ++handlers_called; + } + ); + + asio::steady_timer timer(c.get_executor()); + timer.expires_after(std::chrono::seconds(2)); + timer.async_wait([&](auto) { c.cancel(); }); + + ioc.run(); + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + + +BOOST_AUTO_TEST_CASE(cancel_multiple_ops) { + using test::after; + using namespace std::chrono; + + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + + auto begin = high_resolution_clock::now(); + + // packets + auto connect = encoders::encode_connect( + "", std::nullopt, std::nullopt, 10, false, {}, std::nullopt + ); + auto connack = encoders::encode_connack( + false, reason_codes::success.value(), {} + ); + auto publish_1 = encoders::encode_publish( + 65535, "t_1", "p_1", qos_e::at_least_once, retain_e::no, dup_e::no, {} + ); + auto puback = encoders::encode_puback( + 65535, reason_codes::success.value(), {} + ); + + test::msg_exchange broker_side; + error_code success{}; + error_code fail = asio::error::not_connected; + + broker_side + .expect(connect) + .complete_with(success, after(10ms)) + .reply_with(connack, after(20ms)) + .expect(publish_1) + .complete_with(success, after(10s)) + .reply_with(puback, after(10s)); + //.send(publish_1, after(10s)); + + asio::io_context ioc; + auto executor = ioc.get_executor(); + asio::make_service(ioc, executor, std::move(broker_side)); + + using client_type = mqtt_client; + client_type c(executor, ""); + c.brokers("127.0.0.1") + .run(); + + c.async_publish( + "t_1", "p_1", retain_e::no, publish_props{}, + [&](error_code ec, reason_code rc, auto) { + BOOST_CHECK_MESSAGE(ec, ec.message()); + BOOST_CHECK_MESSAGE(rc, rc.message()); + ++handlers_called; + } + ); + + asio::steady_timer timer(c.get_executor()); + timer.expires_after(std::chrono::seconds(2)); + timer.async_wait([&](auto) { c.cancel(); }); + + ioc.run(); + auto end = high_resolution_clock::now(); + auto duration = duration_cast(end - begin); + + BOOST_CHECK_MESSAGE( + duration <= std::chrono::seconds(3), + "The client did not cancel properly!" + ); + + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/coroutine.cpp b/test/unit/test/coroutine.cpp new file mode 100644 index 0000000..2e3a238 --- /dev/null +++ b/test/unit/test/coroutine.cpp @@ -0,0 +1,139 @@ +#include + +#include + +#include +#include +#include +#include +#include + +#include + +BOOST_AUTO_TEST_SUITE(coroutine/*, *boost::unit_test::disabled()*/) + +using namespace async_mqtt5; +namespace asio = boost::asio; + +template +asio::awaitable sanity_check(mqtt_client& c) { + co_await c.template async_publish( + "test/mqtt-test", "hello world with qos0!", retain_e::no, publish_props{}, + asio::use_awaitable + ); + + auto [puback_rc, puback_props] = co_await c.template async_publish( + "test/mqtt-test", "hello world with qos1!", + retain_e::no, publish_props{}, + asio::use_awaitable + ); + BOOST_CHECK(!puback_rc); + + auto [pubcomp_rc, pubcomp_props] = co_await c.template async_publish( + "test/mqtt-test", "hello world with qos2!", + retain_e::no, publish_props{}, + asio::use_awaitable + ); + BOOST_CHECK(!pubcomp_rc); + + std::vector topics; + topics.push_back(subscribe_topic{ + "test/mqtt-test", { + qos_e::exactly_once, + subscribe_options::no_local_e::no, + subscribe_options::retain_as_published_e::retain, + subscribe_options::retain_handling_e::send + } + }); + + auto [sub_codes, sub_props] = co_await c.async_subscribe( + topics, subscribe_props{}, asio::use_awaitable + ); + BOOST_CHECK(!sub_codes[0]); + auto [topic, payload, publish_props] = co_await c.async_receive(asio::use_awaitable); + + auto [unsub_codes, unsub_props] = co_await c.async_unsubscribe( + std::vector{"test/mqtt-test"}, unsubscribe_props{}, + asio::use_awaitable + ); + BOOST_CHECK(!unsub_codes[0]); + + co_await c.async_disconnect(asio::use_awaitable); + co_return; +} + + +BOOST_AUTO_TEST_CASE(tcp_client_check) { + asio::io_context ioc; + + using stream_type = asio::ip::tcp::socket; + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("tcp-tester", "", "") + .brokers("mqtt.mireo.local", 1883) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + asio::steady_timer timer(ioc); + timer.expires_after(std::chrono::seconds(10)); + + timer.async_wait( + [&](boost::system::error_code ec) { + BOOST_CHECK_MESSAGE(ec, "Failed to receive all the expected replies!"); + c.cancel(); + ioc.stop(); + } + ); + + co_spawn(ioc, + [&]() -> asio::awaitable { + co_await sanity_check(c); + timer.cancel(); + }, + asio::detached + ); + + ioc.run(); +} + +// TODO: SSL + +BOOST_AUTO_TEST_CASE(websocket_tcp_client_check) { + asio::io_context ioc; + + using stream_type = boost::beast::websocket::stream< + asio::ip::tcp::socket + >; + + using client_type = mqtt_client; + client_type c(ioc, ""); + + c.credentials("websocket-tcp-tester", "", "") + .brokers("fcluster-5/mqtt", 8083) + .will({ "test/mqtt-test", "i died", qos_e::at_least_once }) + .run(); + + asio::steady_timer timer(ioc); + timer.expires_after(std::chrono::seconds(10)); + + timer.async_wait( + [&](boost::system::error_code ec) { + BOOST_CHECK_MESSAGE(ec, "Failed to receive all the expected replies!"); + c.cancel(); + ioc.stop(); + } + ); + + co_spawn(ioc, + [&]() -> asio::awaitable { + co_await sanity_check(c); + timer.cancel(); + }, + asio::detached + ); + + ioc.run(); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/publish_send_op.cpp b/test/unit/test/publish_send_op.cpp new file mode 100644 index 0000000..3c20d15 --- /dev/null +++ b/test/unit/test/publish_send_op.cpp @@ -0,0 +1,139 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "test_common/test_service.hpp" + +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 < + typename StreamType, + typename TlsContext = std::monostate +> +class overrun_client : public detail::client_service { +public: + overrun_client(const asio::any_io_executor& ex, const std::string& cnf) : + detail::client_service(ex, cnf) + {} + + uint16_t allocate_pid() { + return 0; + } +}; + +BOOST_AUTO_TEST_CASE(test_pid_overrun) { + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + + asio::io_context ioc; + using client_service_type = overrun_client; + auto svc_ptr = std::make_shared(ioc.get_executor(), ""); + + auto handler = [&](error_code ec, reason_code rc, puback_props) { + ++handlers_called; + BOOST_CHECK_EQUAL(ec, client::error::pid_overrun); + BOOST_CHECK_EQUAL(rc, reason_codes::empty); + }; + + detail::publish_send_op< + client_service_type, decltype(handler), qos_e::at_least_once + > { svc_ptr, std::move(handler) } + .perform( + "test", "payload", retain_e::no, {} + ); + + ioc.run(); + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + +BOOST_AUTO_TEST_CASE(test_publish_immediate_cancellation) { + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + + asio::io_context ioc; + using client_service_type = test::test_service; + auto svc_ptr = std::make_shared(ioc.get_executor()); + asio::cancellation_signal cancel_signal; + + auto h = [&](error_code ec, reason_code rc, puback_props) { + ++handlers_called; + BOOST_CHECK_EQUAL(ec, asio::error::operation_aborted); + BOOST_CHECK_EQUAL(rc, reason_codes::empty); + }; + + auto handler = asio::bind_cancellation_slot(cancel_signal.slot(), std::move(h)); + + detail::publish_send_op< + client_service_type, decltype(handler), qos_e::at_least_once + > { svc_ptr, std::move(handler) } + .perform( + "test", "payload", retain_e::no, {} + ); + + cancel_signal.emit(asio::cancellation_type::terminal); + ioc.run(); + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + +BOOST_AUTO_TEST_CASE(test_publish_cancellation) { + constexpr int expected_handlers_called = 1; + int handlers_called = 0; + + asio::io_context ioc; + using client_service_type = test::test_service; + auto svc_ptr = std::make_shared(ioc.get_executor()); + asio::cancellation_signal cancel_signal; + + auto h = [&](error_code ec, reason_code rc, puback_props) { + ++handlers_called; + BOOST_CHECK_EQUAL(ec, asio::error::operation_aborted); + BOOST_CHECK_EQUAL(rc, reason_codes::empty); + }; + + auto handler = asio::bind_cancellation_slot(cancel_signal.slot(), std::move(h)); + + asio::steady_timer timer(ioc.get_executor()); + timer.expires_after(std::chrono::milliseconds(60)); + timer.async_wait( + [&cancel_signal](error_code) { + cancel_signal.emit(asio::cancellation_type::terminal); + } + ); + + detail::publish_send_op< + client_service_type, decltype(handler), qos_e::at_least_once + > { svc_ptr, std::move(handler) } + .perform( + "test", "payload", retain_e::no, {} + ); + + ioc.run(); + BOOST_CHECK_EQUAL( + handlers_called, expected_handlers_called + ); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit/test/serialization.cpp b/test/unit/test/serialization.cpp new file mode 100644 index 0000000..7ed7d1e --- /dev/null +++ b/test/unit/test/serialization.cpp @@ -0,0 +1,298 @@ +#include + +#include +#include +#include + +using namespace async_mqtt5; +using byte_citer = detail::byte_citer; + +BOOST_AUTO_TEST_SUITE(serialization/*, *boost::unit_test::disabled()*/) + +BOOST_AUTO_TEST_CASE(test_connect) { + // testing variables + std::string_view client_id = "async_mqtt_client_id"; + std::string_view uname = "username"; + std::optional password = std::nullopt; + uint16_t keep_alive = 60; + bool clean_start = true; + std::string will_topic = "will_topic"; + std::string will_message = "will_message"; + + connect_props cprops; + cprops[prop::session_expiry_interval] = 29; + cprops[prop::user_property].emplace_back("connect user prop"); + + will w{ will_topic, will_message }; + w[prop::content_type] = "will content type"; + w[prop::response_topic] = "will response topic"; + w[prop::user_property].emplace_back("first user prop"); + w[prop::user_property].emplace_back("second user prop"); + std::optional will_opt { std::move(w) }; + + auto msg = encoders::encode_connect( + client_id, uname, password, keep_alive, clean_start, cprops, will_opt + ); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing CONNECT fixed header failed."); + + const auto& [control_byte, remain_length] = *header; + auto rv = decoders::decode_connect(remain_length, it); + BOOST_CHECK_MESSAGE(rv, "Parsing CONNECT failed."); + + const auto& [client_id_, uname_, password_, keep_alive_, clean_start_, _, w_] = *rv; + BOOST_CHECK_EQUAL(client_id_, client_id); + BOOST_CHECK(uname_); + BOOST_CHECK_EQUAL(*uname_, uname); + BOOST_CHECK(password_ == password); + BOOST_CHECK_EQUAL(keep_alive_, keep_alive); + BOOST_CHECK_EQUAL(clean_start_, clean_start); + BOOST_CHECK(w_); + BOOST_CHECK_EQUAL((*w_).topic(), will_topic); + BOOST_CHECK_EQUAL((*w_).message(), will_message); + +} + +BOOST_AUTO_TEST_CASE(test_connack) { + // testing variables + bool session_present = true; + uint8_t reason_code = 0x89; + bool wildcard_sub = true; + + connack_props cap; + cap[prop::wildcard_subscription_available] = wildcard_sub; + auto msg = encoders::encode_connack(session_present, reason_code, cap); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing CONNACK fixed header failed."); + + const auto& [control_byte, remain_length] = *header; + auto rv = decoders::decode_connack(remain_length, it); + BOOST_CHECK_MESSAGE(rv, "Parsing CONNACK failed."); + + const auto& [session_present_, reason_code_, caprops] = *rv; + BOOST_CHECK_EQUAL(session_present_, session_present); + BOOST_CHECK_EQUAL(reason_code_, reason_code); + BOOST_CHECK_EQUAL(*caprops[prop::wildcard_subscription_available], wildcard_sub); +} + +BOOST_AUTO_TEST_CASE(test_publish) { + // testing variables + uint16_t packet_id = 31283; + std::string_view topic = "publish_topic"; + std::string_view payload = "This is some payload I am publishing!"; + uint8_t message_expiry = 70; + std::string content_type = "application/octet-stream"; + std::string publish_prop_1 = "first publish prop"; + std::string publish_prop_2 = "second publish prop"; + + publish_props pp; + pp[prop::message_expiry_interval] = message_expiry; + pp[prop::content_type] = content_type; + pp[prop::user_property].emplace_back(publish_prop_1); + pp[prop::user_property].emplace_back(publish_prop_2); + + auto msg = encoders::encode_publish( + packet_id, topic, payload, + qos_e::at_least_once, retain_e::yes, dup_e::no, + pp + ); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing PUBLISH fixed header failed."); + + const auto& [control_byte, remain_length] = *header; + auto rv = decoders::decode_publish(control_byte, remain_length, it); + BOOST_CHECK_MESSAGE(rv, "Parsing PUBLISH failed."); + + const auto& [topic_, packet_id_, flags, pprops, payload_] = *rv; + BOOST_CHECK(packet_id); + BOOST_CHECK_EQUAL(*packet_id_, packet_id); + BOOST_CHECK_EQUAL(topic_, topic); + BOOST_CHECK_EQUAL(payload_, payload_); + BOOST_CHECK_EQUAL(*pprops[prop::message_expiry_interval], message_expiry); + BOOST_CHECK_EQUAL(*pprops[prop::content_type], content_type); + BOOST_CHECK_EQUAL(pprops[prop::user_property][0], publish_prop_1); + BOOST_CHECK_EQUAL(pprops[prop::user_property][1], publish_prop_2); +} + +BOOST_AUTO_TEST_CASE(test_puback) { + // testing variables + uint16_t packet_id = 9199; + uint8_t reason_code = 0x93; + std::string reason_string = "PUBACK reason string"; + std::string user_prop = "PUBACK user prop"; + + puback_props pp; + pp[prop::reason_string] = reason_string; + pp[prop::user_property].emplace_back(user_prop); + + auto msg = encoders::encode_puback(packet_id, reason_code, pp); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing PUBACK fixed header failed."); + + auto packet_id_ = decoders::decode_packet_id(it); + BOOST_CHECK(packet_id); + BOOST_CHECK_EQUAL(*packet_id_, packet_id); + + const auto& [control_byte, remain_length] = *header; + auto rv = decoders::decode_puback(remain_length, it); + BOOST_CHECK_MESSAGE(rv, "Parsing PUBACK failed."); + + const auto& [reason_code_, pprops] = *rv; + BOOST_CHECK_EQUAL(reason_code_, reason_code); + BOOST_CHECK_EQUAL(*pprops[prop::reason_string], reason_string); + BOOST_CHECK_EQUAL(pprops[prop::user_property][0], user_prop); +} + +BOOST_AUTO_TEST_CASE(test_subscribe) { + subscribe_props sp; + std::vector filters { + { "subscribe topic", { qos_e::at_least_once } } + }; + uint16_t packet_id = 65535; + + auto msg = encoders::encode_subscribe(packet_id, filters, sp); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing SUBSCRIBE fixed header failed."); + + const auto& [control_byte, remain_length] = *header; + auto packet_id_ = decoders::decode_packet_id(it); + BOOST_CHECK(packet_id); + BOOST_CHECK_EQUAL(*packet_id_, packet_id); + auto rv = decoders::decode_subscribe(remain_length - sizeof(uint16_t), it); + BOOST_CHECK_MESSAGE(rv, "Parsing SUBSCRIBE failed."); + + const auto& [props_, filters_] = *rv; + BOOST_CHECK_EQUAL(filters[0].topic_filter, std::get<0>(filters_[0])); + //TODO: sub options +} + +BOOST_AUTO_TEST_CASE(test_suback) { + //testing variables + suback_props sp; + std::vector reason_codes { 48, 28 }; + uint16_t packet_id = 142; + + auto msg = encoders::encode_suback(packet_id, reason_codes, sp); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing SUBACK fixed header failed."); + + const auto& [control_byte, remain_length] = *header; + auto packet_id_ = decoders::decode_packet_id(it); + BOOST_CHECK(packet_id); + BOOST_CHECK_EQUAL(*packet_id_, packet_id); + auto rv = decoders::decode_suback(remain_length - sizeof(uint16_t), it); + BOOST_CHECK_MESSAGE(rv, "Parsing SUBACK failed."); + + const auto& [sp_, reason_codes_] = *rv; + BOOST_CHECK(reason_codes_ == reason_codes); +} + +BOOST_AUTO_TEST_CASE(test_unsubscribe) { + // testing variables + unsubscribe_props sp; + std::vector topics { "first topic", "second/topic" }; + uint16_t packet_id = 14423; + + auto msg = encoders::encode_unsubscribe(packet_id, topics, sp); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing UNSUBSCRIBE fixed header failed."); + + const auto& [control_byte, remain_length] = *header; + auto packet_id_ = decoders::decode_packet_id(it); + BOOST_CHECK(packet_id); + BOOST_CHECK_EQUAL(*packet_id_, packet_id); + auto rv = decoders::decode_unsubscribe(remain_length - sizeof(uint16_t), it); + BOOST_CHECK_MESSAGE(rv, "Parsing UNSUBSCRIBE failed."); + + const auto& [props_, topics_] = *rv; + BOOST_CHECK(topics_ == topics); +} + +BOOST_AUTO_TEST_CASE(test_unsuback) { + // testing variables + std::string reason_string = "some unsuback reason string"; + unsuback_props sp; + sp[prop::reason_string] = reason_string; + std::vector reason_codes { 48, 28 }; + uint16_t packet_id = 42; + + auto msg = encoders::encode_unsuback(packet_id, reason_codes, sp); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing UNSUBACK fixed header failed."); + + const auto& [control_byte, remain_length] = *header; + auto packet_id_ = decoders::decode_packet_id(it); + BOOST_CHECK(packet_id); + BOOST_CHECK_EQUAL(*packet_id_, packet_id); + auto rv = decoders::decode_unsuback(remain_length - sizeof(uint16_t), it); + BOOST_CHECK_MESSAGE(header, "Parsing UNSUBACK failed."); + + const auto& [props_, reason_codes_] = *rv; + BOOST_CHECK(reason_codes_ == reason_codes); + BOOST_CHECK_EQUAL(*props_[prop::reason_string], reason_string); +} + +BOOST_AUTO_TEST_CASE(test_disconnect) { + // testing variables + uint8_t reason_code = 0; + std::string user_property = "DISCONNECT user property"; + disconnect_props sp; + sp[prop::user_property].emplace_back(user_property); + + auto msg = encoders::encode_disconnect(reason_code, sp); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing DISCONNECT fixed header failed."); + + const auto& [control_byte, remain_length] = *header; + auto rv = decoders::decode_disconnect(remain_length, it); + BOOST_CHECK_MESSAGE(header, "Parsing DISCONNECT failed."); + + const auto& [reason_code_, props_] = *rv; + BOOST_CHECK_EQUAL(reason_code_, reason_code); + BOOST_CHECK_EQUAL(props_[prop::user_property][0], user_property); +} + +BOOST_AUTO_TEST_CASE(test_auth) { + // testing variables + uint8_t reason_code = 0x18; + std::string reason_string = "AUTH reason"; + std::string user_property = "AUTH user propety"; + auth_props sp; + sp[prop::reason_string] = reason_string; + sp[prop::user_property].emplace_back(user_property); + + auto msg = encoders::encode_auth(reason_code, sp); + + byte_citer it = msg.cbegin(), last = msg.cend(); + auto header = decoders::decode_fixed_header(it, last); + BOOST_CHECK_MESSAGE(header, "Parsing AUTH fixed header failed."); + + const auto& [control_byte, remain_length] = *header; + auto rv = decoders::decode_auth(remain_length, it); + BOOST_CHECK_MESSAGE(rv, "Parsing AUTH failed."); + + const auto& [reason_code_, props_] = *rv; + BOOST_CHECK_EQUAL(reason_code_, reason_code); + BOOST_CHECK_EQUAL(*props_[prop::reason_string], reason_string); + BOOST_CHECK_EQUAL(props_[prop::user_property][0], user_property); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/win/mqtt-client.sln b/win/mqtt-client.sln new file mode 100644 index 0000000..a37fdd5 --- /dev/null +++ b/win/mqtt-client.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33110.190 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mqtt-client", "mqtt-client.vcxproj", "{303BA426-05B5-47FB-9F00-E932257FB28F}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mqtt-test", "test/mqtt-test.vcxproj", "{48DEAF83-BA87-4FDE-8A4C-A539BA44A479}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {303BA426-05B5-47FB-9F00-E932257FB28F}.Debug|x64.ActiveCfg = Debug|x64 + {303BA426-05B5-47FB-9F00-E932257FB28F}.Debug|x64.Build.0 = Debug|x64 + {303BA426-05B5-47FB-9F00-E932257FB28F}.Debug|x86.ActiveCfg = Debug|Win32 + {303BA426-05B5-47FB-9F00-E932257FB28F}.Debug|x86.Build.0 = Debug|Win32 + {303BA426-05B5-47FB-9F00-E932257FB28F}.Release|x64.ActiveCfg = Release|x64 + {303BA426-05B5-47FB-9F00-E932257FB28F}.Release|x64.Build.0 = Release|x64 + {303BA426-05B5-47FB-9F00-E932257FB28F}.Release|x86.ActiveCfg = Release|Win32 + {303BA426-05B5-47FB-9F00-E932257FB28F}.Release|x86.Build.0 = Release|Win32 + {48DEAF83-BA87-4FDE-8A4C-A539BA44A479}.Debug|x64.ActiveCfg = Debug|x64 + {48DEAF83-BA87-4FDE-8A4C-A539BA44A479}.Debug|x64.Build.0 = Debug|x64 + {48DEAF83-BA87-4FDE-8A4C-A539BA44A479}.Debug|x86.ActiveCfg = Debug|Win32 + {48DEAF83-BA87-4FDE-8A4C-A539BA44A479}.Debug|x86.Build.0 = Debug|Win32 + {48DEAF83-BA87-4FDE-8A4C-A539BA44A479}.Release|x64.ActiveCfg = Release|x64 + {48DEAF83-BA87-4FDE-8A4C-A539BA44A479}.Release|x64.Build.0 = Release|x64 + {48DEAF83-BA87-4FDE-8A4C-A539BA44A479}.Release|x86.ActiveCfg = Release|Win32 + {48DEAF83-BA87-4FDE-8A4C-A539BA44A479}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E5126E4A-FF26-4E63-BC54-CE092A8E7966} + EndGlobalSection +EndGlobal diff --git a/win/mqtt-client.vcxproj b/win/mqtt-client.vcxproj new file mode 100644 index 0000000..d010973 --- /dev/null +++ b/win/mqtt-client.vcxproj @@ -0,0 +1,189 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 16.0 + Win32Proj + {303ba426-05b5-47fb-9f00-e932257fb28f} + mqttclient + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level4 + true + NOMINMAX;_CRT_SECURE_NO_WARNINGS;_CRT_NON_CONFORMING_SWPRINTFS;WIN32_LEAN_AND_MEAN;WIN32;_WIN32_WINDOWS=0x0601;_WIN32_WINNT=0x0601;_DEBUG;_CONSOLE;_FILE_OFFSET_BITS=64;BOOST_ALL_NO_LIB;BOOST_UUID_RANDOM_PROVIDER_FORCE_WINCRYPT;%(PreprocessorDefinitions) + true + stdcpp20 + /bigobj %(AdditionalOptions) + ..\include;$(DevRoot)3rdParty\openssl\include;%(AdditionalIncludeDirectories) + + + Console + true + crypt32.lib;openssl.lib;%(AdditionalDependencies) + $(DevRoot)Components\Bin\libd\x64\MT + + + + + Level4 + true + true + true + NOMINMAX;_CRT_SECURE_NO_WARNINGS;_CRT_NON_CONFORMING_SWPRINTFS;WIN32_LEAN_AND_MEAN;WIN32;_WIN32_WINDOWS=0x0601;_WIN32_WINNT=0x0601;_CONSOLE;NDEBUG;_FILE_OFFSET_BITS=64;BOOST_ALL_NO_LIB;BOOST_UUID_RANDOM_PROVIDER_FORCE_WINCRYPT;%(PreprocessorDefinitions) + true + stdcpp20 + ..\include;$(DevRoot)3rdParty\openssl\include;%(AdditionalIncludeDirectories) + + + Console + true + true + true + crypt32.lib;openssl.lib;%(AdditionalDependencies) + $(DevRoot)Components\Bin\libd\x64\MT + + + + + + \ No newline at end of file diff --git a/win/mqtt-client.vcxproj.filters b/win/mqtt-client.vcxproj.filters new file mode 100644 index 0000000..1766729 --- /dev/null +++ b/win/mqtt-client.vcxproj.filters @@ -0,0 +1,174 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {fc537c67-a947-4840-80e0-6fe3f3d5d460} + + + {b786d33e-710a-4a1c-bc48-a14f6d79399c} + + + {0ce770cf-e2fa-429c-94f5-0e70835bed03} + + + {ec0b6550-a767-411d-9cea-1e33bcd63aab} + + + {e3af9391-bfab-4e35-aad7-0e5b8541ac1c} + + + {e83887a6-c73a-45ca-b224-45de8f660c74} + + + {ab9e608c-0bf9-4e0a-8dfe-1e0a2e040718} + + + + + Header Files\include\detail + + + Header Files\include\detail + + + Header Files\include\detail + + + Header Files\include\detail + + + Header Files\include\detail + + + Header Files\include\detail + + + Header Files\include\detail + + + Header Files\include\impl\internal\alloc + + + Header Files\include\impl\internal\alloc + + + Header Files\include\impl\internal\alloc + + + Header Files\include\impl\internal\alloc + + + Header Files\include\impl\internal\codecs + + + Header Files\include\impl\internal\codecs + + + Header Files\include\impl\internal\codecs + + + Header Files\include\impl\internal\codecs + + + Header Files\include\impl\internal\codecs + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include\impl + + + Header Files\include + + + Header Files\include + + + Header Files\include + + + Header Files\include + + + Header Files + + + + + Source Files\example + + + Source Files\example + + + Source Files\example + + + Source Files\example + + + Source Files + + + \ No newline at end of file diff --git a/win/test/mqtt-test.vcxproj b/win/test/mqtt-test.vcxproj new file mode 100644 index 0000000..b4dcce6 --- /dev/null +++ b/win/test/mqtt-test.vcxproj @@ -0,0 +1,158 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + 16.0 + Win32Proj + {48deaf83-ba87-4fde-8a4c-a539ba44a479} + mqtttest + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + NOMINMAX;_CRT_SECURE_NO_WARNINGS;_CRT_NON_CONFORMING_SWPRINTFS;WIN32_LEAN_AND_MEAN;WIN32;_WIN32_WINDOWS=0x0601;_WIN32_WINNT=0x0601;BOOST_TEST_NO_MAIN=1;_DEBUG;_CONSOLE;_FILE_OFFSET_BITS=64;BOOST_ALL_NO_LIB;BOOST_UUID_RANDOM_PROVIDER_FORCE_WINCRYPT;%(PreprocessorDefinitions) + true + stdcpp20 + Async + /bigobj %(AdditionalOptions) + ../../include;../../test/unit/include;%(AdditionalIncludeDirectories) + + + Console + true + crypt32.lib;%(AdditionalDependencies) + + + + + Level3 + true + true + true + NOMINMAX;_CRT_SECURE_NO_WARNINGS;_CRT_NON_CONFORMING_SWPRINTFS;WIN32_LEAN_AND_MEAN;WIN32;_WIN32_WINDOWS=0x0601;_WIN32_WINNT=0x0601;BOOST_TEST_NO_MAIN=1;NDEBUG;_CONSOLE;_FILE_OFFSET_BITS=64;BOOST_ALL_NO_LIB;BOOST_UUID_RANDOM_PROVIDER_FORCE_WINCRYPT;%(PreprocessorDefinitions) + true + stdcpp20 + Async + /bigobj %(AdditionalOptions) + ../../include;../../test/unit/include;%(AdditionalIncludeDirectories) + + + Console + true + true + true + crypt32.lib;%(AdditionalDependencies) + + + + + + \ No newline at end of file diff --git a/win/test/mqtt-test.vcxproj.filters b/win/test/mqtt-test.vcxproj.filters new file mode 100644 index 0000000..a09cc39 --- /dev/null +++ b/win/test/mqtt-test.vcxproj.filters @@ -0,0 +1,63 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {237ad32b-00b2-42f0-8b54-e4f62a437742} + + + {6361a7f6-7954-4ea3-a2a7-f3b15537a3ac} + + + + + Source Files\test + + + Source Files\test + + + Source Files\test + + + Source Files\test + + + Source Files + + + + + Header Files\test_common + + + Header Files\test_common + + + Header Files\test_common + + + Header Files\test_common + + + Header Files\test_common + + + Header Files\test_common + + + Header Files\test_common + + + \ No newline at end of file