diff --git a/.github/workflows/ci-downstream.yml b/.github/workflows/ci-downstream.yml index d54d780..34f45b2 100644 --- a/.github/workflows/ci-downstream.yml +++ b/.github/workflows/ci-downstream.yml @@ -12,13 +12,54 @@ on: jobs: call-opendaq-reusable: name: LT Streaming Modern - uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@v1 + uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@v2 with: opendaq-ref: ${{ github.event.inputs.opendaq-ref || '' }} + include-jobs: > + [ + "windows-2022-x86-msvs-v143-release", + "windows-2022-x86_64-msvs-v143-debug", + "ubuntu-20.04-x86-ninja-gcc-7-release", + "ubuntu-20.04-x86_64-ninja-clang-9-release", + "ubuntu-24.04-x86_64-ninja-gcc-14-debug", + "ubuntu-24.04-x86_64-ninja-gcc-14-release", + "ubuntu-24.04-x86_64-ninja-clang-18-release", + "macos-15-x86_64-ninja-appleclang-release", + "macos-26-x86_64-ninja-appleclang-debug", + "macos-26-armv8-ninja-appleclang-release" + ] + packages: > + [ + { + "match-jobs": ["ubuntu-*-x86_64-*"], + "apt-install": ["libssl-dev"] + }, + { + "match-jobs": ["ubuntu-*-x86-*"], + "apt-install": ["libssl-dev:i386", "lld", "libx11-dev:i386", "libxi-dev:i386", "libxcursor-dev:i386", "libxrandr-dev:i386", "libgl-dev:i386", "libudev-dev:i386", "libfreetype6-dev:i386"] + }, + { + "match-jobs": ["windows-*-x86-*"], + "run": "vcpkg install openssl:x86-windows; Add-Content -Path $env:GITHUB_PATH -Value C:\\vcpkg\\installed\\x86-windows\\bin -Encoding utf8" + } + ] cmake-presets: > [ { "configure-preset": "module", "test-preset": "module-test" + }, + { + "match-jobs": ["windows-*-x86-*"], + "cache-variables": { "OPENSSL_ROOT_DIR": "C:/vcpkg/installed/x86-windows" } + }, + { + "match-jobs": ["ubuntu-*-x86-*"], + "cache-variables": { + "OPENDAQ_FORCE_COMPILE_32BIT": true, + "OPENDAQ_GENERATE_PYTHON_BINDINGS": false, + "OPENDAQ_GENERATE_CSHARP_BINDINGS": false, + "CMAKE_CXX_FLAGS_INIT": "-Wno-error=shift-count-overflow" + } } ] diff --git a/CMakeLists.txt b/CMakeLists.txt index a3c56f7..02dcbe0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,3 +77,8 @@ add_subdirectory(external) add_subdirectory(shared) add_subdirectory(modules) +# End-to-end integration tests need both modules present +if (${REPO_OPTION_PREFIX}_ENABLE_TESTS AND ${REPO_OPTION_PREFIX}_ENABLE_CLIENT AND ${REPO_OPTION_PREFIX}_ENABLE_SERVER) + add_subdirectory(tests) +endif() + diff --git a/external/ws-streaming/CMakeLists.txt b/external/ws-streaming/CMakeLists.txt index d9a5b0b..262fb38 100644 --- a/external/ws-streaming/CMakeLists.txt +++ b/external/ws-streaming/CMakeLists.txt @@ -2,10 +2,9 @@ set(WS_STREAMING_INSTALL OFF) opendaq_dependency( NAME ws-streaming - REQUIRED_VERSION 3.0.7 + REQUIRED_VERSION 3.0.8 GIT_REPOSITORY https://github.com/openDAQ/ws-streaming - GIT_REF 9d591b39ec4845a9f65c558411d809800d455a5d + GIT_REF tls + GIT_SHALLOW OFF EXPECT_TARGET ws-streaming::ws-streaming - PATCH_FILES - ${CMAKE_CURRENT_SOURCE_DIR}/patches/0001-Fixes-plus.patch ) diff --git a/external/ws-streaming/patches/0001-Fixes-plus.patch b/external/ws-streaming/patches/0001-Fixes-plus.patch deleted file mode 100644 index 57b413a..0000000 --- a/external/ws-streaming/patches/0001-Fixes-plus.patch +++ /dev/null @@ -1,545 +0,0 @@ -diff --git a/include/ws-streaming/connection.hpp b/include/ws-streaming/connection.hpp -index bf3c0ec..2471f0e 100644 ---- a/include/ws-streaming/connection.hpp -+++ b/include/ws-streaming/connection.hpp -@@ -62,6 +62,7 @@ namespace wss - connection( - boost::asio::ip::tcp::socket&& socket, - bool is_client, -+ std::string local_stream_id, - bool use_tcp_protocol = false); - - /** -diff --git a/include/ws-streaming/detail/peer.hpp b/include/ws-streaming/detail/peer.hpp -index 7c07a2b..8970385 100644 ---- a/include/ws-streaming/detail/peer.hpp -+++ b/include/ws-streaming/detail/peer.hpp -@@ -368,7 +368,7 @@ namespace wss::detail - template - void enqueue( - const ConstBufferSequence& buffers, -- std::size_t size, -+ std::size_t /*size*/, - bool do_shutdown_after) - { - std::size_t bytes_buffered = boost::asio::buffer_copy( -diff --git a/include/ws-streaming/listener.hpp b/include/ws-streaming/listener.hpp -index fc402d3..1d2494a 100644 ---- a/include/ws-streaming/listener.hpp -+++ b/include/ws-streaming/listener.hpp -@@ -105,9 +105,13 @@ namespace wss - - void do_accept() - { -- using namespace std::placeholders; - acceptor.async_accept( -- std::bind(&listener::finish_accept, this->shared_from_this(), _1, _2)); -+ [self_weak = this->weak_from_this()](auto&& ec, auto&& socket) { -+ if (auto self = self_weak.lock()) { -+ self->finish_accept(ec, std::forward(socket)); -+ } -+ } -+ ); - } - - void finish_accept( -diff --git a/src/client.cpp b/src/client.cpp -index bda2e68..031892a 100644 ---- a/src/client.cpp -+++ b/src/client.cpp -@@ -58,9 +58,22 @@ void wss::client::async_connect( - if (response.result() != boost::beast::http::status::switching_protocols) - return handler(boost::beast::http::error::bad_status, {}); - -+ std::string connection_local_stream_id; -+ try -+ { -+ auto remote_endpoint = stream.socket().remote_endpoint(); -+ connection_local_stream_id = remote_endpoint.address().to_string() -+ + ":" + std::to_string(remote_endpoint.port()); -+ } -+ catch (const std::exception& /*e*/) -+ { -+ return; -+ } -+ - auto connection = std::make_shared( - stream.release_socket(), -- true); -+ true, -+ connection_local_stream_id); - - auto data = buffer.data(); - connection->run(data.data(), data.size()); -@@ -99,9 +112,22 @@ void wss::client::async_connect( - if (ec) - return handler(ec, {}); - -+ std::string connection_local_stream_id; -+ try -+ { -+ auto remote_endpoint = socket->remote_endpoint(); -+ connection_local_stream_id = remote_endpoint.address().to_string() -+ + ":" + std::to_string(remote_endpoint.port()); -+ } -+ catch (const std::exception& /*e*/) -+ { -+ return; -+ } -+ - auto connection = std::make_shared( - std::move(*socket), - true, -+ connection_local_stream_id, - true); - - connection->run(); -diff --git a/src/connection.cpp b/src/connection.cpp -index b0a1c52..ff8b9cc 100644 ---- a/src/connection.cpp -+++ b/src/connection.cpp -@@ -28,11 +28,11 @@ using namespace std::placeholders; - wss::connection::connection( - boost::asio::ip::tcp::socket&& socket, - bool is_client, -+ std::string local_stream_id, - bool use_tcp_protocol) - : _is_client{is_client} - , _peer{std::make_shared(std::move(socket), is_client, use_tcp_protocol)} -- , _local_stream_id{_peer->socket().remote_endpoint().address().to_string() -- + ":" + std::to_string(_peer->socket().remote_endpoint().port())} -+ , _local_stream_id{local_stream_id} - { - _command_interfaces["jsonrpc"] = { { "httpMethod", "" } }; - } -@@ -51,9 +51,25 @@ void wss::connection::register_external_command_interface( - - void wss::connection::run() - { -- _on_peer_data_received = _peer->on_data_received.connect(std::bind(&connection::on_peer_data_received, shared_from_this(), _1, _2, _3)); -- _on_peer_metadata_received = _peer->on_metadata_received.connect(std::bind(&connection::on_peer_metadata_received, shared_from_this(), _1, _2, _3)); -- _on_peer_closed = _peer->on_closed.connect(std::bind(&connection::on_peer_closed, shared_from_this(), _1)); -+ auto self_weak = weak_from_this(); -+ -+ _on_peer_data_received = _peer->on_data_received.connect( -+ [self_weak](unsigned signo, const std::uint8_t* data, std::size_t size) { -+ if (auto self = self_weak.lock()) -+ self->on_peer_data_received(signo, data, size); -+ }); -+ -+ _on_peer_metadata_received = _peer->on_metadata_received.connect( -+ [self_weak](unsigned signo, const std::string& method, const nlohmann::json& params) { -+ if (auto self = self_weak.lock()) -+ self->on_peer_metadata_received(signo, method, params); -+ }); -+ -+ _on_peer_closed = _peer->on_closed.connect( -+ [self_weak](const boost::system::error_code& ec) { -+ if (auto self = self_weak.lock()) -+ self->on_peer_closed(ec); -+ }); - - _peer->run(); - -@@ -63,9 +79,28 @@ void wss::connection::run() - - void wss::connection::run(const void *data, std::size_t size) - { -- _on_peer_data_received = _peer->on_data_received.connect(std::bind(&connection::on_peer_data_received, shared_from_this(), _1, _2, _3)); -- _on_peer_metadata_received = _peer->on_metadata_received.connect(std::bind(&connection::on_peer_metadata_received, shared_from_this(), _1, _2, _3)); -- _on_peer_closed = _peer->on_closed.connect(std::bind(&connection::on_peer_closed, shared_from_this(), _1)); -+ auto self_weak = weak_from_this(); -+ -+ _on_peer_data_received = _peer->on_data_received.connect( -+ [self_weak](unsigned signo, const std::uint8_t* data, std::size_t size) -+ { -+ if (auto self = self_weak.lock()) -+ self->on_peer_data_received(signo, data, size); -+ }); -+ -+ _on_peer_metadata_received = _peer->on_metadata_received.connect( -+ [self_weak](unsigned signo, const std::string& method, const nlohmann::json& params) -+ { -+ if (auto self = self_weak.lock()) -+ self->on_peer_metadata_received(signo, method, params); -+ }); -+ -+ _on_peer_closed = _peer->on_closed.connect( -+ [self_weak](const boost::system::error_code& ec) -+ { -+ if (auto self = self_weak.lock()) -+ self->on_peer_closed(ec); -+ }); - - _peer->run(data, size); - -@@ -266,6 +301,7 @@ void wss::connection::on_local_signal_metadata_changed( - } - } - -+ entry.value_index = 0; - _peer->send_metadata(entry.signo, "signal", entry.signal.metadata().json()); - } - -@@ -316,7 +352,7 @@ void wss::connection::on_signal_subscribe_requested( - return; // @todo XXX TODO - - _command_interface_client->async_request(_remote_stream_id + ".subscribe", { signal_id }, -- [](const boost::system::error_code& ec, const nlohmann::json& response) -+ [](const boost::system::error_code& /*ec*/, const nlohmann::json& /*response*/) - { - }); - } -@@ -328,7 +364,7 @@ void wss::connection::on_signal_unsubscribe_requested( - return; // @todo XXX TODO - - _command_interface_client->async_request(_remote_stream_id + ".unsubscribe", { signal_id }, -- [](const boost::system::error_code& ec, const nlohmann::json& response) -+ [](const boost::system::error_code& /*ec*/, const nlohmann::json& /*response*/) - { - }); - } -@@ -615,21 +651,25 @@ bool wss::connection::subscribe( - "signal", - metadata); - -+ auto self_weak = weak_from_this(); -+ auto signal_weak = std::weak_ptr(signal); -+ - signal->on_data_published = signal->signal.on_data_published.connect( -- std::bind( -- &connection::on_local_signal_data_published, -- shared_from_this(), -- signal, -- _1, -- _2, -- _3, -- _4)); -+ [self_weak, signal_weak](std::int64_t domain_value, -+ std::size_t sample_count, -+ const void* data, -+ std::size_t size) { -+ if (auto self = self_weak.lock()) -+ if (auto s = signal_weak.lock()) -+ self->on_local_signal_data_published(s, domain_value, sample_count, data, size); -+ }); - - signal->on_metadata_changed = signal->signal.on_metadata_changed.connect( -- std::bind( -- &connection::on_local_signal_metadata_changed, -- shared_from_this(), -- std::ref(*signal))); -+ [self_weak, signal_weak]() { -+ if (auto self = self_weak.lock()) -+ if (auto s = signal_weak.lock()) -+ self->on_local_signal_metadata_changed(*s); -+ }); - - return true; - } -diff --git a/src/detail/command_interface_client_factory.cpp b/src/detail/command_interface_client_factory.cpp -index 7f8eea0..f101571 100644 ---- a/src/detail/command_interface_client_factory.cpp -+++ b/src/detail/command_interface_client_factory.cpp -@@ -38,9 +38,20 @@ wss::detail::command_interface_client_factory::create_client( - else - port = interfaces["jsonrpc-http"]["port"]; - -+ std::string remote_endpoint_address; -+ try -+ { -+ auto remote_endpoint = peer->socket().remote_endpoint(); -+ remote_endpoint_address = remote_endpoint.address().to_string(); -+ } -+ catch (const std::exception& /*e*/) -+ { -+ return nullptr; -+ } -+ - return std::make_unique( - peer->socket().get_executor(), -- peer->socket().remote_endpoint().address().to_string(), -+ remote_endpoint_address, - port, - interfaces["jsonrpc-http"]["httpMethod"], - interfaces["jsonrpc-http"]["httpPath"], -diff --git a/src/detail/http_client.cpp b/src/detail/http_client.cpp -index a64aa03..211c1f6 100644 ---- a/src/detail/http_client.cpp -+++ b/src/detail/http_client.cpp -@@ -53,11 +53,12 @@ void wss::detail::http_client::async_request( - _resolver.async_resolve( - hostname, - port, -- std::bind( -- &http_client::finish_resolve, -- shared_from_this(), -- std::placeholders::_1, -- std::placeholders::_2)); -+ [self_weak = weak_from_this()](const boost::system::error_code& ec, -+ const boost::asio::ip::tcp::resolver::results_type& results) -+ { -+ if (auto self = self_weak.lock()) -+ self->finish_resolve(ec, results); -+ }); - } - - void wss::detail::http_client::cancel() -@@ -75,10 +76,11 @@ void wss::detail::http_client::finish_resolve( - - _stream.async_connect( - results, -- std::bind( -- &http_client::finish_connect, -- shared_from_this(), -- std::placeholders::_1)); -+ [self_weak = weak_from_this()](const boost::system::error_code& ec, auto /*endpoint*/) -+ { -+ if (auto self = self_weak.lock()) -+ self->finish_connect(ec); // finish_connect only takes ec -+ }); - } - - void wss::detail::http_client::finish_connect( -@@ -92,10 +94,11 @@ void wss::detail::http_client::finish_connect( - boost::beast::http::async_write( - _stream, - _request, -- std::bind( -- &http_client::finish_write, -- shared_from_this(), -- std::placeholders::_1)); -+ [self_weak = weak_from_this()](const boost::system::error_code& ec, std::size_t /*bytes_transferred*/) -+ { -+ if (auto self = self_weak.lock()) -+ self->finish_write(ec); -+ }); - } - - void wss::detail::http_client::finish_write( -@@ -110,10 +113,11 @@ void wss::detail::http_client::finish_write( - _stream, - _buffer, - _response, -- std::bind( -- &http_client::finish_read, -- shared_from_this(), -- std::placeholders::_1)); -+ [self_weak = weak_from_this()](const boost::system::error_code& ec, std::size_t /*bytes_transferred*/) -+ { -+ if (auto self = self_weak.lock()) -+ self->finish_read(ec); -+ }); - } - - void wss::detail::http_client::finish_read( -diff --git a/src/detail/http_client_servicer.cpp b/src/detail/http_client_servicer.cpp -index eeb1352..86be5f6 100644 ---- a/src/detail/http_client_servicer.cpp -+++ b/src/detail/http_client_servicer.cpp -@@ -40,9 +40,10 @@ void wss::detail::http_client_servicer::stop() - { - boost::asio::post( - stream.get_executor(), -- [self = shared_from_this()]() -+ [self_weak = weak_from_this()]() - { -- self->close(); -+ if (auto self = self_weak.lock()) -+ self->close(); - }); - } - -@@ -54,11 +55,11 @@ void wss::detail::http_client_servicer::do_read() - stream, - buffer, - req = {}, -- std::bind( -- &http_client_servicer::finish_read, -- shared_from_this(), -- _1, -- _2)); -+ [self_weak = weak_from_this()](const boost::system::error_code& ec, std::size_t bytes_transferred) -+ { -+ if (auto self = self_weak.lock()) -+ self->finish_read(ec, bytes_transferred); -+ }); - } - - void wss::detail::http_client_servicer::do_write( -@@ -68,17 +69,16 @@ void wss::detail::http_client_servicer::do_write( - boost::beast::async_write( - stream, - std::move(msg), -- std::bind( -- &http_client_servicer::finish_write, -- shared_from_this(), -- action, -- _1, -- _2)); -+ [self_weak = weak_from_this(), action](const boost::system::error_code& ec, std::size_t bytes_transferred) -+ { -+ if (auto self = self_weak.lock()) -+ self->finish_write(action, ec, bytes_transferred); -+ }); - } - - void wss::detail::http_client_servicer::finish_read( - const boost::system::error_code& ec, -- std::size_t bytes_transferred) -+ std::size_t /*bytes_transferred*/) - { - if (ec) - return close(ec); -@@ -197,7 +197,7 @@ void wss::detail::http_client_servicer::finish_read( - void wss::detail::http_client_servicer::finish_write( - response_actions action, - const boost::beast::error_code& ec, -- std::size_t bytes_transferred) -+ std::size_t /*bytes_transferred*/) - { - if (ec) - return close(ec); -diff --git a/src/detail/http_command_interface_client.cpp b/src/detail/http_command_interface_client.cpp -index 1932625..ea1244d 100644 ---- a/src/detail/http_command_interface_client.cpp -+++ b/src/detail/http_command_interface_client.cpp -@@ -66,8 +66,8 @@ void wss::detail::http_command_interface_client::async_request( - [this, handler = std::move(handler), client]( - const boost::system::error_code& ec, - const boost::beast::http::response& response, -- boost::beast::tcp_stream& stream, -- const boost::beast::flat_buffer& buffer) -+ boost::beast::tcp_stream& /*stream*/, -+ const boost::beast::flat_buffer& /*buffer*/) - { - _clients.erase(client); - -@@ -84,7 +84,7 @@ void wss::detail::http_command_interface_client::async_request( - response_json = nlohmann::json::parse(response.body()); - } - -- catch (const nlohmann::json::exception& ex) -+ catch (const nlohmann::json::exception& /*ex*/) - { - } - -diff --git a/src/detail/peer.cpp b/src/detail/peer.cpp -index 0a24fd2..10d8b01 100644 ---- a/src/detail/peer.cpp -+++ b/src/detail/peer.cpp -@@ -25,7 +25,7 @@ using namespace std::placeholders; - - wss::detail::peer::peer( - boost::asio::ip::tcp::socket&& socket, -- bool is_client, -+ bool /*is_client*/, - bool use_tcp_protocol, - std::size_t rx_buffer_size, - std::size_t tx_buffer_size) -@@ -48,9 +48,10 @@ void wss::detail::peer::run(const void *data, std::size_t size) - if (size > _rx_buffer.size()) - boost::asio::post( - _socket.get_executor(), -- [self = shared_from_this()]() -+ [self_weak = weak_from_this()]() - { -- self->close(boost::asio::error::no_buffer_space); -+ if (auto self = self_weak.lock()) -+ self->close(boost::asio::error::no_buffer_space); - }); - - std::memcpy( -@@ -62,9 +63,10 @@ void wss::detail::peer::run(const void *data, std::size_t size) - - boost::asio::post( - _socket.get_executor(), -- [self = shared_from_this()]() -+ [self_weak = weak_from_this()]() - { -- self->process_buffer(); -+ if (auto self = self_weak.lock()) -+ self->process_buffer(); - }); - } - -@@ -115,20 +117,22 @@ void wss::detail::peer::do_wait_rx() - { - _socket.async_wait( - boost::asio::socket_base::wait_read, -- std::bind( -- &peer::finish_wait_rx, -- shared_from_this(), -- _1)); -+ [self_weak = weak_from_this()](const boost::system::error_code& ec) -+ { -+ if (auto self = self_weak.lock()) -+ self->finish_wait_rx(ec); -+ }); - } - - void wss::detail::peer::do_wait_tx() - { - _socket.async_wait( - boost::asio::socket_base::wait_write, -- std::bind( -- &peer::finish_wait_tx, -- shared_from_this(), -- _1)); -+ [self_weak = weak_from_this()](const boost::system::error_code& ec) -+ { -+ if (auto self = self_weak.lock()) -+ self->finish_wait_tx(ec); -+ }); - - _waiting_tx = true; - } -diff --git a/src/server.cpp b/src/server.cpp -index d915db1..a743df7 100644 ---- a/src/server.cpp -+++ b/src/server.cpp -@@ -116,7 +116,7 @@ void wss::server::on_listener_accept( - } - - nlohmann::json wss::server::on_servicer_command_interface_request( -- const std::shared_ptr& servicer, -+ const std::shared_ptr& /*servicer*/, - const std::string& method, - const nlohmann::json& params) - { -@@ -139,12 +139,24 @@ nlohmann::json wss::server::on_servicer_command_interface_request( - } - - void wss::server::on_servicer_websocket_upgrade( -- const std::shared_ptr& servicer, -+ const std::shared_ptr& /*servicer*/, - boost::asio::ip::tcp::socket& socket) - { -+ std::string connection_local_stream_id; -+ try -+ { -+ auto remote_endpoint = socket.remote_endpoint(); -+ connection_local_stream_id = remote_endpoint.address().to_string() -+ + ":" + std::to_string(remote_endpoint.port()); -+ } -+ catch (const std::exception& /*e*/) -+ { -+ return; -+ } - auto connection = std::make_shared( - std::move(socket), -- false); -+ false, -+ connection_local_stream_id); - - if (_command_interface_port) - connection->register_external_command_interface( -@@ -189,7 +201,7 @@ void wss::server::on_servicer_websocket_upgrade( - - void wss::server::on_servicer_closed( - const std::shared_ptr& servicer, -- const boost::system::error_code& ec) -+ const boost::system::error_code& /*ec*/) - { - _sessions.remove_if([&](const client_entry& entry) - { diff --git a/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/common.h b/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/common.h index 53b1e95..c55896e 100644 --- a/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/common.h +++ b/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/common.h @@ -19,3 +19,17 @@ #define BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_CLIENT_MODULE BEGIN_NAMESPACE_OPENDAQ_MODULE(websocket_streaming_client_module) #define END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_CLIENT_MODULE END_NAMESPACE_OPENDAQ_MODULE + +#if !defined(DAQMODULES_LT_STREAMING_ENABLE_TESTS) + #define DAQ_WS_STREAM_CL_MODULE_API +#else + #if defined(_WIN32) + #if defined(OPENDAQ_MODULE_DLL_IMPORT) + #define DAQ_WS_STREAM_CL_MODULE_API __declspec(dllimport) + #else + #define DAQ_WS_STREAM_CL_MODULE_API __declspec(dllexport) + #endif + #else + #define DAQ_WS_STREAM_CL_MODULE_API __attribute__((visibility("default"))) + #endif +#endif diff --git a/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/websocket_streaming_client_module_impl.h b/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/websocket_streaming_client_module_impl.h index 500f56c..b2a2857 100644 --- a/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/websocket_streaming_client_module_impl.h +++ b/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/websocket_streaming_client_module_impl.h @@ -19,6 +19,8 @@ #include #include +class WebsocketStreamingClientModuleTest; + BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_CLIENT_MODULE class WebsocketStreamingClientModule final : public Module @@ -32,18 +34,31 @@ class WebsocketStreamingClientModule final : public Module DevicePtr onCreateDevice(const StringPtr& connectionString, const ComponentPtr& parent, const PropertyObjectPtr& config) override; - bool acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config); - bool acceptsStreamingConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config); + DAQ_WS_STREAM_CL_MODULE_API bool acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config); + DAQ_WS_STREAM_CL_MODULE_API bool acceptsStreamingConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config); StreamingPtr onCreateStreaming(const StringPtr& connectionString, const PropertyObjectPtr& config) override; Bool onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) override; private: - static StringPtr createUrlConnectionString(const StringPtr& host, - const IntegerPtr& port, - const StringPtr& path); - static PropertyObjectPtr createDefaultConfig(); - static StringPtr formConnectionString(const StringPtr& connectionString, const PropertyObjectPtr& config); + friend class ::WebsocketStreamingClientModuleTest; + + struct ConnectionParameters + { + std::string host; + std::uint16_t port = 0; + std::string prefix; + std::string path; + }; + + DAQ_WS_STREAM_CL_MODULE_API static StringPtr createUrlConnectionString(const StringPtr& host, + const IntegerPtr& port, + const StringPtr& path); + DAQ_WS_STREAM_CL_MODULE_API static StringPtr formConnectionString(const StringPtr& connectionString, + const PropertyObjectPtr& config, + ConnectionParameters* outParams = nullptr); + DAQ_WS_STREAM_CL_MODULE_API static StringPtr formNewStyleConnectionString(const StringPtr& connectionString); static DeviceInfoPtr populateDiscoveredDevice(const discovery::MdnsDiscoveredDevice& discoveredDevice); + DAQ_WS_STREAM_CL_MODULE_API static bool isSecureConnection(const std::string& connectionString); std::mutex sync; size_t deviceIndex; diff --git a/modules/websocket_streaming_client_module/src/CMakeLists.txt b/modules/websocket_streaming_client_module/src/CMakeLists.txt index 57f1af8..b4b7176 100644 --- a/modules/websocket_streaming_client_module/src/CMakeLists.txt +++ b/modules/websocket_streaming_client_module/src/CMakeLists.txt @@ -30,6 +30,10 @@ if (MSVC) target_compile_options(${LIB_NAME} PRIVATE /bigobj) endif() +if (${REPO_OPTION_PREFIX}_ENABLE_TESTS) + target_compile_definitions(${LIB_NAME} PRIVATE ${REPO_OPTION_PREFIX}_ENABLE_TESTS) +endif() + target_link_libraries(${LIB_NAME} PUBLIC daq::opendaq diff --git a/modules/websocket_streaming_client_module/src/websocket_streaming_client_module_impl.cpp b/modules/websocket_streaming_client_module/src/websocket_streaming_client_module_impl.cpp index db65658..a20eda0 100644 --- a/modules/websocket_streaming_client_module/src/websocket_streaming_client_module_impl.cpp +++ b/modules/websocket_streaming_client_module/src/websocket_streaming_client_module_impl.cpp @@ -19,6 +19,8 @@ #include #include #include +#include "boost/algorithm/string/replace.hpp" +#include "websocket_streaming/constants.h" #include #include @@ -70,9 +72,13 @@ DictPtr WebsocketStreamingClientModule::onGetAvailableDevi const auto websocketDeviceType = WsStreamingDevice::createNewType(); const auto oldWebsocketDeviceType = WsStreamingDevice::createOldType(); + const auto secureWebsocketDeviceType = WsStreamingDevice::createNewSecureType(); + const auto oldSecureWebsocketDeviceType = WsStreamingDevice::createOldSecureType(); result.set(websocketDeviceType.getId(), websocketDeviceType); result.set(oldWebsocketDeviceType.getId(), oldWebsocketDeviceType); + result.set(secureWebsocketDeviceType.getId(), secureWebsocketDeviceType); + result.set(oldSecureWebsocketDeviceType.getId(), oldSecureWebsocketDeviceType); return result; } @@ -82,8 +88,10 @@ DictPtr WebsocketStreamingClientModule::onGetAvailableS auto result = Dict(); auto websocketStreamingType = WsStreaming::createType(); + auto secureWebsocketStreamingType = WsStreaming::createSecureType(); result.set(websocketStreamingType.getId(), websocketStreamingType); + result.set(secureWebsocketStreamingType.getId(), secureWebsocketStreamingType); return result; } @@ -104,49 +112,35 @@ DevicePtr WebsocketStreamingClientModule::onCreateDevice(const StringPtr& connec // We don't create any streaming objects here since the // internal streaming object is always created within the device - const StringPtr strPtr = formConnectionString(connectionString, config); - const std::string str = strPtr; + ConnectionParameters params; + const StringPtr formedConnectionStr = formConnectionString(connectionString, config, ¶ms); + + PropertyObjectPtr deviceConfig = config; + if (!deviceConfig.assigned()) + deviceConfig = isSecureConnection(formedConnectionStr) ? WsStreamingDevice::createDefaultSecureConfig() + : WsStreamingDevice::createDefaultConfig(); std::scoped_lock lock(sync); std::string localId = fmt::format("websocket_pseudo_device{}", deviceIndex++); - auto deviceType = WsStreamingDevice::createNewType(); + auto deviceType = + isSecureConnection(formedConnectionStr) ? WsStreamingDevice::createNewSecureType() : WsStreamingDevice::createNewType(); checkErrorInfo(deviceType.asPtr()->setModuleInfo(moduleInfo)); - auto device = createWithImplementation(context, parent, localId, strPtr, deviceType); + auto device = createWithImplementation(context, parent, localId, formedConnectionStr, deviceType, deviceConfig); // Set the connection info for the device - auto host = String(""); - auto port = -1; - { - std::smatch match; - - bool parsed = false; - parsed = std::regex_search(str, match, RegexIpv6Hostname); - if (!parsed) - { - parsed = std::regex_search(str, match, RegexIpv4Hostname); - } + StreamingTypePtr wsStreamingType = + (isSecureConnection(formedConnectionStr)) ? WsStreaming::createSecureType() : WsStreaming::createType(); - if (parsed) - { - host = match[2].str(); - port = 7414; - if (match[3].matched) - port = std::stoi(match[3]); - } - } - - // Set the connection info for the device - auto wsStreamingType = WsStreaming::createType(); ServerCapabilityConfigPtr connectionInfo = device.getInfo().getConfigurationConnectionInfo(); connectionInfo.setProtocolId(wsStreamingType.getId()); connectionInfo.setProtocolName(wsStreamingType.getId()); connectionInfo.setProtocolType(ProtocolType::Streaming); connectionInfo.setConnectionType("TCP/IP"); - connectionInfo.addAddress(host); - connectionInfo.setPort(port); + connectionInfo.addAddress(params.host); + connectionInfo.setPort(params.port); connectionInfo.setPrefix(wsStreamingType.getConnectionStringPrefix()); - connectionInfo.setConnectionString(strPtr); + connectionInfo.setConnectionString(formedConnectionStr); return device; } @@ -154,9 +148,13 @@ DevicePtr WebsocketStreamingClientModule::onCreateDevice(const StringPtr& connec bool WebsocketStreamingClientModule::acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& /*config*/) { std::string connStr = connectionString; - auto found = connStr.find(WsStreamingDevice::createOldType().getConnectionStringPrefix().toStdString() + "://") == 0 - || connStr.find(WsStreamingDevice::createNewType().getConnectionStringPrefix().toStdString() + "://") == 0; - return found; + const auto deviceTypes = onGetAvailableDeviceTypes(); + for (const auto& [_, deviceType] : deviceTypes) + { + if (connStr.find(deviceType.getConnectionStringPrefix().toStdString() + "://") == 0) + return true; + } + return false; } bool WebsocketStreamingClientModule::acceptsStreamingConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config) @@ -176,8 +174,14 @@ StreamingPtr WebsocketStreamingClientModule::onCreateStreaming(const StringPtr& if (!acceptsStreamingConnectionParameters(connectionString, config)) DAQ_THROW_EXCEPTION(InvalidParameterException); - const StringPtr str = formConnectionString(connectionString, config); - return createWithImplementation(str, context); + PropertyObjectPtr streamingConfig = config; + if (!streamingConfig.assigned()) + streamingConfig = isSecureConnection(formNewStyleConnectionString(connectionString).toStdString()) + ? WsStreaming::createDefaultSecureConfig() + : WsStreaming::createDefaultConfig(); + + const StringPtr str = formConnectionString(connectionString, streamingConfig); + return createWithImplementation(str, context, streamingConfig); } Bool WebsocketStreamingClientModule::onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) @@ -204,9 +208,9 @@ Bool WebsocketStreamingClientModule::onCompleteServerCapability(const ServerCapa auto port = target.getPort(); if (port == -1) { - port = 7414; + port = DEFAULT_WS_STREAMING_PORT; target.setPort(port); - LOG_W("LT server capability is missing port. Defaulting to 7414.") + LOG_W("LT server capability is missing port. Defaulting to {}", std::to_string(DEFAULT_WS_STREAMING_PORT)) } const auto path = target.hasProperty("Path") ? target.getPropertyValue("Path") : ""; @@ -245,28 +249,20 @@ StringPtr WebsocketStreamingClientModule::createUrlConnectionString(const String + fmt::format("://{}:{}{}", host, port, path)); } -PropertyObjectPtr WebsocketStreamingClientModule::createDefaultConfig() +StringPtr WebsocketStreamingClientModule::formConnectionString(const StringPtr& connectionString, + const PropertyObjectPtr& config, + ConnectionParameters* outParams) { - auto obj = PropertyObject(); - obj.addProperty(IntProperty("Port", 7414)); - return obj; -} - -StringPtr WebsocketStreamingClientModule::formConnectionString(const StringPtr& connectionString, const PropertyObjectPtr& config) -{ - int port = 7414; - if (config.assigned() && config.hasProperty("Port")) - port = config.getPropertyValue("Port"); - - std::string urlString = connectionString.toStdString(); + std::string urlString = formNewStyleConnectionString(connectionString).toStdString(); + bool isSecure = isSecureConnection(urlString); + bool portFromConfig = false; + bool portFromConfigIsDefault = false; std::smatch match; + ConnectionParameters localParams; + if (outParams == nullptr) + outParams = &localParams; - std::string host = ""; - std::string prefix = ""; - std::string path = "/"; - - bool parsed = false; - parsed = std::regex_search(urlString, match, RegexIpv6Hostname); + bool parsed = std::regex_search(urlString, match, RegexIpv6Hostname); if (!parsed) { parsed = std::regex_search(urlString, match, RegexIpv4Hostname); @@ -274,22 +270,51 @@ StringPtr WebsocketStreamingClientModule::formConnectionString(const StringPtr& if (parsed) { - prefix = match[1]; - host = match[2]; + outParams->prefix = match[1]; + outParams->host = match[2]; if (match[3].matched) - port = std::stoi(match[3]); - - if (port == 7414) - return connectionString; + outParams->port = std::stoi(match[3]); if (match[4].matched) - path = match[4]; + outParams->path = match[4]; + + if (outParams->port == 0 && config.assigned()) + { + if ((portFromConfig = (isSecure && config.hasProperty(PROPERTY_WSS_STREAMING_PORT_CLIENT)))) + { + outParams->port = config.getPropertyValue(PROPERTY_WSS_STREAMING_PORT_CLIENT); + portFromConfigIsDefault = outParams->port == DEFAULT_WSS_STREAMING_PORT; + } + else if ((portFromConfig = (!isSecure && config.hasProperty(PROPERTY_WS_STREAMING_PORT_CLIENT)))) + { + outParams->port = config.getPropertyValue(PROPERTY_WS_STREAMING_PORT_CLIENT); + portFromConfigIsDefault = outParams->port == DEFAULT_WS_STREAMING_PORT; + } + } - return prefix + host + ":" + std::to_string(port) + path; } + else + { + DAQ_THROW_EXCEPTION(InvalidParameterException, "Could not parse connection string: {}", connectionString); + } + if (outParams->port == 0) + outParams->port = isSecure ? DEFAULT_WSS_STREAMING_PORT : DEFAULT_WS_STREAMING_PORT; + + std::string output = outParams->prefix + outParams->host; + if (match[3].matched || (portFromConfig && !portFromConfigIsDefault)) + output += ":" + std::to_string(outParams->port); + if (match[4].matched) + output += outParams->path; + return output; +} - return connectionString; +StringPtr WebsocketStreamingClientModule::formNewStyleConnectionString(const StringPtr& connectionString) +{ + auto wsConnectionString = connectionString.toStdString(); + boost::replace_all(wsConnectionString, "daq.ws://", "daq.lt://"); + boost::replace_all(wsConnectionString, "daq.wss://", "daq.lts://"); + return wsConnectionString; } DeviceInfoPtr WebsocketStreamingClientModule::populateDiscoveredDevice(const MdnsDiscoveredDevice& discoveredDevice) @@ -347,4 +372,10 @@ DeviceInfoPtr WebsocketStreamingClientModule::populateDiscoveredDevice(const Mdn WsStreamingDevice::createNewType()); } +bool WebsocketStreamingClientModule::isSecureConnection(const std::string& connectionString) +{ + const auto securePrefix = WsStreaming::createSecureType().getConnectionStringPrefix().toStdString() + "://"; + return connectionString.find(securePrefix) != std::string::npos; +} + END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_CLIENT_MODULE diff --git a/modules/websocket_streaming_client_module/tests/CMakeLists.txt b/modules/websocket_streaming_client_module/tests/CMakeLists.txt index 29b2796..d56cac7 100644 --- a/modules/websocket_streaming_client_module/tests/CMakeLists.txt +++ b/modules/websocket_streaming_client_module/tests/CMakeLists.txt @@ -10,6 +10,8 @@ add_executable(${TEST_APP} ${TEST_SOURCES} target_link_libraries(${TEST_APP} PRIVATE daq::opendaq_test_utils gtest ${OPENDAQ_SDK_TARGET_NAMESPACE}::${MODULE_NAME} + daq::discovery + daq::websocket_streaming ) add_test(NAME ${TEST_APP} diff --git a/modules/websocket_streaming_client_module/tests/test_websocket_streaming_client_module.cpp b/modules/websocket_streaming_client_module/tests/test_websocket_streaming_client_module.cpp index 673815e..5e23f83 100644 --- a/modules/websocket_streaming_client_module/tests/test_websocket_streaming_client_module.cpp +++ b/modules/websocket_streaming_client_module/tests/test_websocket_streaming_client_module.cpp @@ -1,16 +1,54 @@ #include #include #include +#include +#include #include #include #include #include +#include #include +#include -using WebsocketStreamingClientModuleTest = testing::Test; using namespace daq; +using namespace daq::websocket_streaming; +using namespace daq::modules::websocket_streaming_client_module; + +class WebsocketStreamingClientModuleTest : public testing::Test +{ +protected: + using ConnectionParameters = WebsocketStreamingClientModule::ConnectionParameters; + + static StringPtr formConnectionString(const StringPtr& connectionString, + const PropertyObjectPtr& config, + ConnectionParameters* outParams = nullptr) + { + return WebsocketStreamingClientModule::formConnectionString(connectionString, config, outParams); + } + + static StringPtr createUrlConnectionString(const StringPtr& host, const IntegerPtr& port, const StringPtr& path) + { + return WebsocketStreamingClientModule::createUrlConnectionString(host, port, path); + } + + static bool isSecureConnection(const std::string& connectionString) + { + return WebsocketStreamingClientModule::isSecureConnection(connectionString); + } + + static bool acceptsConnectionParameters(const ModulePtr& module, const StringPtr& connectionString, const PropertyObjectPtr& config) + { + return reinterpret_cast(module.getObject())->acceptsConnectionParameters(connectionString, config); + } + + static bool acceptsStreamingConnectionParameters(const ModulePtr& module, const StringPtr& connectionString, const PropertyObjectPtr& config) + { + return reinterpret_cast(module.getObject())->acceptsStreamingConnectionParameters(connectionString, config); + } +}; static ModulePtr CreateModule() { @@ -129,17 +167,35 @@ TEST_F(WebsocketStreamingClientModuleTest, GetAvailableComponentTypes) DictPtr deviceTypes; ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); - ASSERT_EQ(deviceTypes.getCount(), 2u); + ASSERT_EQ(deviceTypes.getCount(), 4u); ASSERT_TRUE(deviceTypes.hasKey("OpenDAQLTStreaming")); ASSERT_EQ(deviceTypes.get("OpenDAQLTStreaming").getId(), "OpenDAQLTStreaming"); + ASSERT_TRUE(deviceTypes.hasKey("OpenDAQLTStreamingSecure")); + ASSERT_EQ(deviceTypes.get("OpenDAQLTStreamingSecure").getId(), "OpenDAQLTStreamingSecure"); ASSERT_TRUE(deviceTypes.hasKey("OpenDAQLTStreamingOld")); ASSERT_EQ(deviceTypes.get("OpenDAQLTStreamingOld").getId(), "OpenDAQLTStreamingOld"); + ASSERT_TRUE(deviceTypes.hasKey("OpenDAQLTStreamingOldSecure")); + ASSERT_EQ(deviceTypes.get("OpenDAQLTStreamingOldSecure").getId(), "OpenDAQLTStreamingOldSecure"); DictPtr serverTypes; ASSERT_NO_THROW(serverTypes = module.getAvailableServerTypes()); ASSERT_EQ(serverTypes.getCount(), 0u); } +TEST_F(WebsocketStreamingClientModuleTest, GetAvailableStreamingTypes) +{ + const auto module = CreateModule(); + + DictPtr streamingTypes; + ASSERT_NO_THROW(streamingTypes = module.getAvailableStreamingTypes()); + ASSERT_EQ(streamingTypes.getCount(), 2u); + + ASSERT_TRUE(streamingTypes.hasKey("OpenDAQLTStreaming")); + ASSERT_EQ(streamingTypes.get("OpenDAQLTStreaming").getId(), "OpenDAQLTStreaming"); + ASSERT_TRUE(streamingTypes.hasKey("OpenDAQLTStreamingSecure")); + ASSERT_EQ(streamingTypes.get("OpenDAQLTStreamingSecure").getId(), "OpenDAQLTStreamingSecure"); +} + TEST_F(WebsocketStreamingClientModuleTest, CreateFunctionBlockIdNull) { auto module = CreateModule(); @@ -154,3 +210,215 @@ TEST_F(WebsocketStreamingClientModuleTest, CreateFunctionBlockIdEmpty) ASSERT_THROW(module.createFunctionBlock("", nullptr, "fb"), NotFoundException); } + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringKeepsPortAndPath) +{ + ConnectionParameters params; + const auto result = formConnectionString("daq.lt://host:1234/foo", nullptr, ¶ms); + + ASSERT_EQ(result, "daq.lt://host:1234/foo"); + ASSERT_EQ(params.host, "host"); + ASSERT_EQ(params.port, 1234); + ASSERT_EQ(params.path, "/foo"); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringDefaultsPath) +{ + ConnectionParameters params; + formConnectionString("daq.lt://host:1234", nullptr, ¶ms); + + ASSERT_EQ(params.path, ""); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringNoPortNoConfig0) +{ + const auto result = formConnectionString("daq.lt://host", nullptr); + const auto expected = std::string("daq.lt://host"); + ASSERT_EQ(result, expected); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringNoPortNoConfig1) +{ + const auto result = formConnectionString("daq.lts://host", nullptr); + const auto expected = std::string("daq.lts://host"); + ASSERT_EQ(result, expected); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringTakesPortFromConfig) +{ + auto config = PropertyObject(); + config.addProperty(IntProperty(PROPERTY_WS_STREAMING_PORT_CLIENT, 7414)); + + ConnectionParameters params; + formConnectionString("daq.lt://host", config, ¶ms); + ASSERT_EQ(params.port, 7414); + + auto secureConfig = PropertyObject(); + secureConfig.addProperty(IntProperty(PROPERTY_WSS_STREAMING_PORT_CLIENT, 7415)); + + ConnectionParameters secureParams; + formConnectionString("daq.lts://host", secureConfig, &secureParams); + ASSERT_EQ(secureParams.port, 7415); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringNormalizesOldStylePrefixes) +{ + ASSERT_EQ(formConnectionString("daq.ws://host:1234/", nullptr), "daq.lt://host:1234/"); + ASSERT_EQ(formConnectionString("daq.wss://host:1234/", nullptr), "daq.lts://host:1234/"); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringParsesIpv6) +{ + ConnectionParameters params; + const auto result = formConnectionString("daq.lt://[::1]:1234/", nullptr, ¶ms); + + ASSERT_EQ(result, "daq.lt://[::1]:1234/"); + ASSERT_EQ(params.host, "[::1]"); + ASSERT_EQ(params.port, 1234); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringIpv6TakesPortFromConfig) +{ + auto config = PropertyObject(); + config.addProperty(IntProperty(PROPERTY_WS_STREAMING_PORT_CLIENT, 7414)); + + ConnectionParameters params; + formConnectionString("daq.lt://[::1]/", config, ¶ms); + + ASSERT_EQ(params.host, "[::1]"); + ASSERT_EQ(params.port, 7414); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringParsesIpv6WithZoneId) +{ + ConnectionParameters params; + ASSERT_NO_THROW(formConnectionString("daq.lt://[fe80::1%eth0]/", nullptr, ¶ms)); + ASSERT_EQ(params.host, "[fe80::1%eth0]"); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringParsesPrefixless) +{ + ConnectionParameters params; + formConnectionString("host:1234", nullptr, ¶ms); + + ASSERT_EQ(params.prefix, ""); + ASSERT_EQ(params.host, "host"); + ASSERT_EQ(params.port, 1234); +} + +TEST_F(WebsocketStreamingClientModuleTest, FormConnectionStringThrowsOnUnparsable) +{ + ASSERT_THROW(formConnectionString("", nullptr), InvalidParameterException); +} + +TEST_F(WebsocketStreamingClientModuleTest, IsSecureConnection) +{ + ASSERT_TRUE(isSecureConnection("daq.lts://h")); + ASSERT_FALSE(isSecureConnection("daq.lt://h")); + ASSERT_FALSE(isSecureConnection("daq.ws://h")); + ASSERT_FALSE(isSecureConnection("daq.wss://h")); +} + +TEST_F(WebsocketStreamingClientModuleTest, AcceptsAllPrefixes) +{ + auto module = CreateModule(); + + ASSERT_TRUE(acceptsConnectionParameters(module, "daq.lt://h", nullptr)); + ASSERT_TRUE(acceptsConnectionParameters(module, "daq.ws://h", nullptr)); + ASSERT_TRUE(acceptsConnectionParameters(module, "daq.lts://h", nullptr)); + ASSERT_TRUE(acceptsConnectionParameters(module, "daq.wss://h", nullptr)); +} + +TEST_F(WebsocketStreamingClientModuleTest, RejectsForeignPrefixes) +{ + auto module = CreateModule(); + + ASSERT_FALSE(acceptsConnectionParameters(module, "daq.opcua://h", nullptr)); + ASSERT_FALSE(acceptsConnectionParameters(module, "daqref://h", nullptr)); + ASSERT_FALSE(acceptsConnectionParameters(module, "daq.nd://h", nullptr)); + ASSERT_FALSE(acceptsConnectionParameters(module, "garbage", nullptr)); + ASSERT_FALSE(acceptsConnectionParameters(module, "x daq.lt://h", nullptr)); +} + +TEST_F(WebsocketStreamingClientModuleTest, AcceptsStreamingRejectsEmpty) +{ + auto module = CreateModule(); + + ASSERT_FALSE(acceptsStreamingConnectionParameters(module, "", nullptr)); + ASSERT_FALSE(acceptsStreamingConnectionParameters(module, nullptr, nullptr)); + ASSERT_TRUE(acceptsStreamingConnectionParameters(module, "daq.lts://h", nullptr)); +} + +TEST_F(WebsocketStreamingClientModuleTest, SecureStreamingDefaultConfigRejectsEmptyCertKey) +{ + auto module = CreateModule(); + ASSERT_THROW_MSG(module.createStreaming("daq.lts://127.0.0.1:1/", nullptr), + InvalidParameterException, + "TLS certificate or key file path is not configured"); +} + +TEST_F(WebsocketStreamingClientModuleTest, SecureStreamingRejectsEmptyCaWithMtlsDisabled) +{ + auto module = CreateModule(); + auto config = module.getAvailableStreamingTypes().get("OpenDAQLTStreamingSecure").createDefaultConfig(); + config.setPropertyValue(PROPERTY_ENABLE_MTLS_CLIENT, False); + + ASSERT_THROW_MSG(module.createStreaming("daq.lts://127.0.0.1:1/", config), + InvalidParameterException, + "TLS CA certificate file path is not configured"); +} + +TEST_F(WebsocketStreamingClientModuleTest, SecureStreamingRejectsEmptyCaWithMtlsEnabled) +{ + auto module = CreateModule(); + auto config = module.getAvailableStreamingTypes().get("OpenDAQLTStreamingSecure").createDefaultConfig(); + config.setPropertyValue(PROPERTY_WSS_CERT_FILE_PATH_CLIENT, "/tmp/cert.pem"); + config.setPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_CLIENT, "/tmp/key.pem"); + + ASSERT_THROW_MSG(module.createStreaming("daq.lts://127.0.0.1:1/", config), + InvalidParameterException, + "TLS CA certificate file path is not configured"); +} + +TEST_F(WebsocketStreamingClientModuleTest, DefaultInsecureStreamingConfig) +{ + auto module = CreateModule(); + auto config = module.getAvailableStreamingTypes().get("OpenDAQLTStreaming").createDefaultConfig(); + ASSERT_TRUE(config.assigned()); + + ASSERT_EQ(config.getAllProperties().getCount(), 1u); + + ASSERT_TRUE(config.hasProperty(PROPERTY_WS_STREAMING_PORT_CLIENT)); + ASSERT_EQ(config.getProperty(PROPERTY_WS_STREAMING_PORT_CLIENT).getValueType(), CoreType::ctInt); + ASSERT_EQ(config.getPropertyValue(PROPERTY_WS_STREAMING_PORT_CLIENT), DEFAULT_WS_STREAMING_PORT); +} + +TEST_F(WebsocketStreamingClientModuleTest, DefaultSecureStreamingConfig) +{ + auto module = CreateModule(); + auto config = module.getAvailableStreamingTypes().get("OpenDAQLTStreamingSecure").createDefaultConfig(); + ASSERT_TRUE(config.assigned()); + + ASSERT_EQ(config.getAllProperties().getCount(), 5u); + + ASSERT_TRUE(config.hasProperty(PROPERTY_WSS_STREAMING_PORT_CLIENT)); + ASSERT_TRUE(config.hasProperty(PROPERTY_ENABLE_MTLS_CLIENT)); + ASSERT_TRUE(config.hasProperty(PROPERTY_WSS_CERT_FILE_PATH_CLIENT)); + ASSERT_TRUE(config.hasProperty(PROPERTY_WSS_KEY_FILE_PATH_CLIENT)); + ASSERT_TRUE(config.hasProperty(PROPERTY_WSS_CA_CERT_FILE_PATH_CLIENT)); + + ASSERT_EQ(config.getProperty(PROPERTY_WSS_STREAMING_PORT_CLIENT).getValueType(), CoreType::ctInt); + ASSERT_EQ(config.getProperty(PROPERTY_ENABLE_MTLS_CLIENT).getValueType(), CoreType::ctBool); + + ASSERT_EQ(config.getPropertyValue(PROPERTY_WSS_STREAMING_PORT_CLIENT), DEFAULT_WSS_STREAMING_PORT); + ASSERT_EQ(config.getPropertyValue(PROPERTY_ENABLE_MTLS_CLIENT), DEFAULT_ENABLE_MTLS); + + config.setPropertyValue(PROPERTY_ENABLE_MTLS_CLIENT, True); + ASSERT_TRUE(config.getProperty(PROPERTY_WSS_CA_CERT_FILE_PATH_CLIENT).getVisible()); + ASSERT_TRUE(config.getProperty(PROPERTY_WSS_CERT_FILE_PATH_CLIENT).getVisible()); + ASSERT_TRUE(config.getProperty(PROPERTY_WSS_KEY_FILE_PATH_CLIENT).getVisible()); + config.setPropertyValue(PROPERTY_ENABLE_MTLS_CLIENT, False); + ASSERT_TRUE(config.getProperty(PROPERTY_WSS_CA_CERT_FILE_PATH_CLIENT).getVisible()); + ASSERT_FALSE(config.getProperty(PROPERTY_WSS_CERT_FILE_PATH_CLIENT).getVisible()); + ASSERT_FALSE(config.getProperty(PROPERTY_WSS_KEY_FILE_PATH_CLIENT).getVisible()); +} \ No newline at end of file diff --git a/modules/websocket_streaming_server_module/tests/CMakeLists.txt b/modules/websocket_streaming_server_module/tests/CMakeLists.txt index 50edcd5..5c73a00 100644 --- a/modules/websocket_streaming_server_module/tests/CMakeLists.txt +++ b/modules/websocket_streaming_server_module/tests/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable(${TEST_APP} ${TEST_SOURCES} target_link_libraries(${TEST_APP} PRIVATE daq::opendaq_test_utils gtest ${OPENDAQ_SDK_TARGET_NAMESPACE}::${MODULE_NAME} + daq::websocket_streaming ) add_test(NAME ${TEST_APP} diff --git a/modules/websocket_streaming_server_module/tests/test_websocket_streaming_server_module.cpp b/modules/websocket_streaming_server_module/tests/test_websocket_streaming_server_module.cpp index ce3cbd4..8bc8447 100644 --- a/modules/websocket_streaming_server_module/tests/test_websocket_streaming_server_module.cpp +++ b/modules/websocket_streaming_server_module/tests/test_websocket_streaming_server_module.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,7 @@ class WsStreamingServerModuleTest : public testing::Test }; using namespace daq; +using namespace daq::websocket_streaming; static ModulePtr CreateModule(ContextPtr context = NullContext()) { @@ -26,6 +28,13 @@ static ModulePtr CreateModule(ContextPtr context = NullContext()) return module; } +static PropertyObjectPtr CreateServerConfig() +{ + auto module = CreateModule(); + auto serverTypes = module.getAvailableServerTypes(); + return serverTypes.get("OpenDAQLTStreaming").createDefaultConfig(); +} + TEST_F(WsStreamingServerModuleTest, CreateModule) { IModule* module = nullptr; @@ -79,16 +88,135 @@ TEST_F(WsStreamingServerModuleTest, GetAvailableComponentTypes) TEST_F(WsStreamingServerModuleTest, ServerConfig) { - auto module = CreateModule(); - - DictPtr serverTypes = module.getAvailableServerTypes(); - ASSERT_TRUE(serverTypes.hasKey("OpenDAQLTStreaming")); - auto config = serverTypes.get("OpenDAQLTStreaming").createDefaultConfig(); + auto config = CreateServerConfig(); ASSERT_TRUE(config.assigned()); - ASSERT_TRUE(config.hasProperty("WebsocketStreamingPort")); - ASSERT_EQ(config.getPropertyValue("WebsocketStreamingPort"), 7414); + ASSERT_EQ(config.getAllProperties().getCount(), 11u); + + ASSERT_TRUE(config.hasProperty(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER).getValueType(), CoreType::ctBool); + ASSERT_EQ(config.getPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER), DEFAULT_ENABLE_WS_STREAMING_PORT); + + ASSERT_TRUE(config.hasProperty(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER).getValueType(), CoreType::ctBool); + ASSERT_EQ(config.getPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER), DEFAULT_ENABLE_WS_CONTROL_PORT); + + ASSERT_TRUE(config.hasProperty(PROPERTY_WS_STREAMING_PORT_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_WS_STREAMING_PORT_SERVER).getValueType(), CoreType::ctInt); + ASSERT_EQ(config.getPropertyValue(PROPERTY_WS_STREAMING_PORT_SERVER), DEFAULT_WS_STREAMING_PORT); + + ASSERT_TRUE(config.hasProperty(PROPERTY_WS_CONTROL_PORT_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_WS_CONTROL_PORT_SERVER).getValueType(), CoreType::ctInt); + ASSERT_EQ(config.getPropertyValue(PROPERTY_WS_CONTROL_PORT_SERVER), DEFAULT_WS_CONTROL_PORT); + + ASSERT_TRUE(config.hasProperty(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER).getValueType(), CoreType::ctBool); + ASSERT_EQ(config.getPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER), DEFAULT_ENABLE_WSS_STREAMING_PORT); + + ASSERT_TRUE(config.hasProperty(PROPERTY_ENABLE_MTLS_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_ENABLE_MTLS_SERVER).getValueType(), CoreType::ctBool); + ASSERT_EQ(config.getPropertyValue(PROPERTY_ENABLE_MTLS_SERVER), DEFAULT_ENABLE_MTLS); + + ASSERT_TRUE(config.hasProperty(PROPERTY_WSS_STREAMING_PORT_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_WSS_STREAMING_PORT_SERVER).getValueType(), CoreType::ctInt); + ASSERT_EQ(config.getPropertyValue(PROPERTY_WSS_STREAMING_PORT_SERVER), DEFAULT_WSS_STREAMING_PORT); + + ASSERT_TRUE(config.hasProperty(PROPERTY_WSS_CERT_FILE_PATH_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_WSS_CERT_FILE_PATH_SERVER).getValueType(), CoreType::ctString); + + ASSERT_TRUE(config.hasProperty(PROPERTY_WSS_KEY_FILE_PATH_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_WSS_KEY_FILE_PATH_SERVER).getValueType(), CoreType::ctString); - ASSERT_TRUE(config.hasProperty("WebsocketControlPort")); - ASSERT_EQ(config.getPropertyValue("WebsocketControlPort"), 7438); + ASSERT_TRUE(config.hasProperty(PROPERTY_WSS_CA_CERT_FILE_PATH_SERVER)); + ASSERT_EQ(config.getProperty(PROPERTY_WSS_CA_CERT_FILE_PATH_SERVER).getValueType(), CoreType::ctString); + + ASSERT_TRUE(config.hasProperty("Path")); + ASSERT_EQ(config.getPropertyValue("Path"), "/"); +} + +TEST_F(WsStreamingServerModuleTest, ServerConfigVisibility) +{ + auto config = CreateServerConfig(); + + config.setPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER, True); + ASSERT_TRUE(config.getProperty(PROPERTY_WS_STREAMING_PORT_SERVER).getVisible()); + config.setPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER, False); + ASSERT_FALSE(config.getProperty(PROPERTY_WS_STREAMING_PORT_SERVER).getVisible()); + + config.setPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER, True); + ASSERT_TRUE(config.getProperty(PROPERTY_WS_CONTROL_PORT_SERVER).getVisible()); + config.setPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER, False); + ASSERT_FALSE(config.getProperty(PROPERTY_WS_CONTROL_PORT_SERVER).getVisible()); + + config.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, False); + ASSERT_FALSE(config.getProperty(PROPERTY_ENABLE_MTLS_SERVER).getVisible()); + ASSERT_FALSE(config.getProperty(PROPERTY_WSS_STREAMING_PORT_SERVER).getVisible()); + ASSERT_FALSE(config.getProperty(PROPERTY_WSS_CERT_FILE_PATH_SERVER).getVisible()); + ASSERT_FALSE(config.getProperty(PROPERTY_WSS_KEY_FILE_PATH_SERVER).getVisible()); + + config.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, True); + ASSERT_TRUE(config.getProperty(PROPERTY_ENABLE_MTLS_SERVER).getVisible()); + ASSERT_TRUE(config.getProperty(PROPERTY_WSS_STREAMING_PORT_SERVER).getVisible()); + ASSERT_TRUE(config.getProperty(PROPERTY_WSS_CERT_FILE_PATH_SERVER).getVisible()); + ASSERT_TRUE(config.getProperty(PROPERTY_WSS_KEY_FILE_PATH_SERVER).getVisible()); + + config.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, True); + config.setPropertyValue(PROPERTY_ENABLE_MTLS_SERVER, True); + ASSERT_TRUE(config.getProperty(PROPERTY_WSS_CA_CERT_FILE_PATH_SERVER).getVisible()); + + config.setPropertyValue(PROPERTY_ENABLE_MTLS_SERVER, False); + ASSERT_FALSE(config.getProperty(PROPERTY_WSS_CA_CERT_FILE_PATH_SERVER).getVisible()); + + config.setPropertyValue(PROPERTY_ENABLE_MTLS_SERVER, True); + config.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, False); + ASSERT_FALSE(config.getProperty(PROPERTY_WSS_CA_CERT_FILE_PATH_SERVER).getVisible()); +} + +TEST_F(WsStreamingServerModuleTest, CreateServerRejectsMtlsWithoutCa) +{ + const auto instance = Instance(); + auto module = CreateModule(instance.getContext()); + + auto config = module.getAvailableServerTypes().get("OpenDAQLTStreaming").createDefaultConfig(); + config.setPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER, False); + config.setPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER, False); + config.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, True); + + ASSERT_THROW_MSG(module.createServer("OpenDAQLTStreaming", instance.getRootDevice(), config), + InvalidParameterException, + "Mutual TLS is enabled but no CA certificate file path is configured"); +} + +TEST_F(WsStreamingServerModuleTest, CreateServerRejectsEmptyCertificatePath) +{ + const auto instance = Instance(); + auto module = CreateModule(instance.getContext()); + + auto config = module.getAvailableServerTypes().get("OpenDAQLTStreaming").createDefaultConfig(); + config.setPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER, False); + config.setPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER, False); + config.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, True); + config.setPropertyValue(PROPERTY_ENABLE_MTLS_SERVER, False); + config.setPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_SERVER, "some-key.pem"); + + ASSERT_THROW_MSG(module.createServer("OpenDAQLTStreaming", instance.getRootDevice(), config), + InvalidParameterException, + "TLS certificate or key file path is not configured"); +} + +TEST_F(WsStreamingServerModuleTest, CreateServerRejectsEmptyKeyPath) +{ + const auto instance = Instance(); + auto module = CreateModule(instance.getContext()); + + auto config = module.getAvailableServerTypes().get("OpenDAQLTStreaming").createDefaultConfig(); + config.setPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER, False); + config.setPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER, False); + config.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, True); + config.setPropertyValue(PROPERTY_ENABLE_MTLS_SERVER, False); + config.setPropertyValue(PROPERTY_WSS_CERT_FILE_PATH_SERVER, "some-cert.pem"); + + ASSERT_THROW_MSG(module.createServer("OpenDAQLTStreaming", instance.getRootDevice(), config), + InvalidParameterException, + "TLS certificate or key file path is not configured"); } diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/constants.h b/shared/libraries/websocket_streaming/include/websocket_streaming/constants.h new file mode 100644 index 0000000..c331e2c --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/constants.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "common.h" + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +static constexpr const char* PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER = "EnableStreamingPort"; +static constexpr const char* PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER = "EnableControlPort"; +static constexpr const char* PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER = "EnableTlsStreamingPort"; +static constexpr const char* PROPERTY_ENABLE_MTLS_SERVER = "EnableMutualTls"; +static constexpr const char* PROPERTY_WS_STREAMING_PORT_SERVER = "WebsocketStreamingPort"; +static constexpr const char* PROPERTY_WS_CONTROL_PORT_SERVER = "WebsocketControlPort"; +static constexpr const char* PROPERTY_WSS_STREAMING_PORT_SERVER = "TlsWebsocketStreamingPort"; +static constexpr const char* PROPERTY_WSS_CERT_FILE_PATH_SERVER = "CertificateFilePath"; +static constexpr const char* PROPERTY_WSS_KEY_FILE_PATH_SERVER = "KeyFilePath"; +static constexpr const char* PROPERTY_WSS_CA_CERT_FILE_PATH_SERVER = "CaCertificateFilePath"; + +static constexpr const char* PROPERTY_ENABLE_MTLS_CLIENT = "EnableMutualTls"; +static constexpr const char* PROPERTY_WS_STREAMING_PORT_CLIENT = "Port"; // to not break existing configs this property is named "Port" instead of "WebsocketStreamingPort" +static constexpr const char* PROPERTY_WSS_STREAMING_PORT_CLIENT = "Port"; +static constexpr const char* PROPERTY_WSS_CERT_FILE_PATH_CLIENT = "CertificateFilePath"; +static constexpr const char* PROPERTY_WSS_KEY_FILE_PATH_CLIENT = "KeyFilePath"; +static constexpr const char* PROPERTY_WSS_CA_CERT_FILE_PATH_CLIENT = "CaCertificateFilePath"; + + +static constexpr bool DEFAULT_ENABLE_WS_STREAMING_PORT = true; +static constexpr bool DEFAULT_ENABLE_WS_CONTROL_PORT = true; +static constexpr bool DEFAULT_ENABLE_WSS_STREAMING_PORT = false; +static constexpr bool DEFAULT_ENABLE_MTLS = true; +static constexpr std::uint16_t DEFAULT_WS_STREAMING_PORT = 7414; +static constexpr std::uint16_t DEFAULT_WS_CONTROL_PORT = 7438; +static constexpr std::uint16_t DEFAULT_WSS_STREAMING_PORT = 7415; +static constexpr const char* DEFAULT_WSS_CERT_FILE_PATH = ""; +static constexpr const char* DEFAULT_WSS_KEY_FILE_PATH = ""; +static constexpr const char* DEFAULT_WSS_CA_CERT_FILE_PATH = ""; + + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming.h b/shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming.h index 8150c0c..65a5ba3 100644 --- a/shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming.h +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming.h @@ -79,6 +79,17 @@ class WsStreaming : public Streaming */ static StreamingTypePtr createType(); + /*! + * @brief Creates an openDAQ streaming type object using the `daq.lts://` prefix + * with secure TLS channel. + * + * @return An openDAQ streaming type object using the `daq.lts://` prefix. + */ + static StreamingTypePtr createSecureType(); + + static PropertyObjectPtr createDefaultConfig(); + static PropertyObjectPtr createDefaultSecureConfig(); + public: /*! @@ -91,7 +102,8 @@ class WsStreaming : public Streaming */ explicit WsStreaming( const StringPtr& connectionString, - const ContextPtr& context); + const ContextPtr& context, + const PropertyObjectPtr& config); /*! * @brief Destroys a streaming object and stops the Boost.Asio I/O context's thread. @@ -126,8 +138,6 @@ class WsStreaming : public Streaming protected: - static PropertyObjectPtr createDefaultConfig(); - void onSetActive(bool active) override; void onAddSignal(const MirroredSignalConfigPtr& signal) override; void onRemoveSignal(const MirroredSignalConfigPtr& signal) override; @@ -163,6 +173,9 @@ class WsStreaming : public Streaming wss::client wsClient; wss::connection_ptr wsConnection; + boost::signals2::scoped_connection onAvailableConnection; + boost::signals2::scoped_connection onUnavailableConnection; + std::map> signals; std::promise promise; diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming_device.h b/shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming_device.h index 93fffad..c17b0d7 100644 --- a/shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming_device.h +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/ws_streaming_device.h @@ -59,6 +59,25 @@ class WsStreamingDevice : public Device */ static DeviceTypePtr createNewType(); + /*! + * @brief Creates an openDAQ device type for the old-style `daq.wss://` connection string + * with secure TLS channel. + * + * @return An openDAQ device type for the old-style `daq.wss://` connection string. + */ + static DeviceTypePtr createOldSecureType(); + + /*! + * @brief Creates an openDAQ device type for the new-style `daq.lts://` connection string + * with secure TLS channel. + * + * @return An openDAQ device type for the new-style `daq.lts://` connection string. + */ + static DeviceTypePtr createNewSecureType(); + + static PropertyObjectPtr createDefaultConfig(); + static PropertyObjectPtr createDefaultSecureConfig(); + /*! * @brief Opens a new WebSocket streaming connection. * @@ -78,12 +97,11 @@ class WsStreamingDevice : public Device const ComponentPtr& parent, const StringPtr& localId, const StringPtr& connectionString, - const DeviceTypePtr& type); + const DeviceTypePtr& type, + const PropertyObjectPtr& config); protected: - static PropertyObjectPtr createDefaultConfig(); - void removed() override; void removedNoLock() override; diff --git a/shared/libraries/websocket_streaming/src/CMakeLists.txt b/shared/libraries/websocket_streaming/src/CMakeLists.txt index 5440df0..4c73434 100644 --- a/shared/libraries/websocket_streaming/src/CMakeLists.txt +++ b/shared/libraries/websocket_streaming/src/CMakeLists.txt @@ -15,6 +15,7 @@ set(SRC_Cpp set(SRC_PublicHeaders common.h + constants.h descriptor_to_metadata.h metadata_to_descriptor.h remote_signal_handler.h diff --git a/shared/libraries/websocket_streaming/src/ws_streaming.cpp b/shared/libraries/websocket_streaming/src/ws_streaming.cpp index 5f2b5a2..010b698 100644 --- a/shared/libraries/websocket_streaming/src/ws_streaming.cpp +++ b/shared/libraries/websocket_streaming/src/ws_streaming.cpp @@ -21,6 +21,8 @@ #include #include +#include +#include #include #include @@ -29,6 +31,7 @@ #include #include +#include "websocket_streaming/constants.h" #include #include @@ -49,26 +52,94 @@ StreamingTypePtr WsStreaming::createType() .build(); } +StreamingTypePtr WsStreaming::createSecureType() +{ + return StreamingTypeBuilder() + .setId("OpenDAQLTStreamingSecure") + .setName("openDAQ WebSocket Streaming") + .setDescription("Streaming from devices using the WebSocket Streaming Protocol and TLS encryption") + .setDefaultConfig(createDefaultSecureConfig()) + .setConnectionStringPrefix("daq.lts") + .build(); +} + WsStreaming::WsStreaming( const StringPtr& connectionString, - const ContextPtr& context) + const ContextPtr& context, + const PropertyObjectPtr& config) : Streaming(connectionString, context, true) , ioContext{1} , wsClient(ioContext.get_executor()) { + // NOTE! The 'port' property is not used there. The formed 'connectionString' + // must contain the port number. + // The ws-streaming library wants a URL like ws://1.2.3.4:7418/foo. - // So we simply need to replace the daq.lt:// prefix with ws://. + // So we simply need to replace the daq.lt:// prefix with ws:// + // and daq.lts:// with wss:// for secure channel auto wsConnectionString = connectionString.toStdString(); boost::replace_all(wsConnectionString, "daq.lt://", "ws://"); boost::replace_all(wsConnectionString, "daq.ws://", "ws://"); + boost::replace_all(wsConnectionString, "daq.lts://", "wss://"); + boost::replace_all(wsConnectionString, "daq.wss://", "wss://"); + bool isSecureChannel = wsConnectionString.find("wss://") != std::string::npos; + + if (isSecureChannel) + { + LOG_I("Secure channel requested, enabling TLS"); + + std::string certFilePath; + std::string keyFilePath; + std::string caCertFilePath; + if (config.hasProperty(PROPERTY_ENABLE_MTLS_CLIENT) && config.getPropertyValue(PROPERTY_ENABLE_MTLS_CLIENT).asPtr() == True) + { + if (!config.hasProperty(PROPERTY_WSS_CERT_FILE_PATH_CLIENT) || !config.hasProperty(PROPERTY_WSS_KEY_FILE_PATH_CLIENT)) + DAQ_THROW_EXCEPTION(InvalidParameterException, + "Mutual TLS is enabled but the configuration has no {} or {} property", + PROPERTY_WSS_CERT_FILE_PATH_CLIENT, PROPERTY_WSS_KEY_FILE_PATH_CLIENT); + + certFilePath = config.getPropertyValue(PROPERTY_WSS_CERT_FILE_PATH_CLIENT).asPtr().toStdString(); + keyFilePath = config.getPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_CLIENT).asPtr().toStdString(); + + if (certFilePath.empty() || keyFilePath.empty()) + DAQ_THROW_EXCEPTION(InvalidParameterException, "TLS certificate or key file path is not configured"); + + LOG_I("mTLS enabled, using cert file: \'{}\' and key file: \'{}\'", certFilePath, keyFilePath); + } + else + { + LOG_I("mTLS disabled"); + } + if (!config.hasProperty(PROPERTY_WSS_CA_CERT_FILE_PATH_CLIENT)) + DAQ_THROW_EXCEPTION(InvalidParameterException, + "A secure connection requires the {} property in the configuration", + PROPERTY_WSS_CA_CERT_FILE_PATH_CLIENT); + + caCertFilePath = config.getPropertyValue(PROPERTY_WSS_CA_CERT_FILE_PATH_CLIENT).asPtr().toStdString(); + if (caCertFilePath.empty()) + DAQ_THROW_EXCEPTION(InvalidParameterException, "TLS CA certificate file path is not configured"); + + LOG_I("Using CA certificate file: \'{}\'", caCertFilePath); + LOG_I("Trying to load TLS secrets..."); + + try + { + wsClient.enable_tls(caCertFilePath, certFilePath, keyFilePath); + } + catch (const std::exception& e) + { + DAQ_THROW_EXCEPTION(InvalidParameterException, "Cannot load the TLS secrets: {}", e.what()); + } + } // Start the ws-streaming connection attempt. LOG_I("Connecting to {}", wsConnectionString); wsClient.async_connect(wsConnectionString, std::bind(&WsStreaming::onConnected, this, _1, _2)); - // Start a background thread to pump the Boost.Asio I/O context. The run() function will - // return when there is no more work or when ioContext.stop() is called in the destructor. + // Start a background thread to pump the Boost.Asio I/O context. The run() function returns + // when there is no more work: on the failed-connection path below via ioContext.stop(), and + // during normal teardown once the destructor closes the connection and the work drains. thread = std::thread{[this] { ioContext.run(); }}; // Wait here until the connection is either established or failed. @@ -78,28 +149,94 @@ WsStreaming::WsStreaming( { ioContext.stop(); thread.join(); - throw NotFoundException( - "Failed to connect to " + connectionString.toStdString() + ": " + std::to_string(ec.value())); + + // A failure raised by the TLS layer means the peer was reached but not trusted. + // That is an authentication problem. + if (ec.category() == boost::asio::error::get_ssl_category()) + DAQ_THROW_EXCEPTION(AuthenticationFailedException, + "Failed to connect to {}: {}", connectionString.toStdString(), ec.message()); + + DAQ_THROW_EXCEPTION(NotFoundException, + "Failed to connect to {}: {}", connectionString.toStdString(), ec.message()); } } WsStreaming::~WsStreaming() { - // Stop the Boost.Asio I/O context (which may already have stopped naturally if the connection - // failed and there is no more scheduled work) so we can join and destroy the thread. - LOG_I("Stopping Boost.Asio I/O context thread"); - ioContext.stop(); + LOG_I("Closing streaming connection and stopping Boost.Asio I/O context thread"); + + // Tear the connection down on the I/O context's thread. The ws-streaming peer is not thread-safe, + // and the connections below and the 'signals' map are only ever touched from that thread, so all + // of this must run there rather than directly from the destructor. + + // Note we deliberately do NOT call ioContext.stop(): stopping abandons in-flight operations + // instead of completing them, which corrupts the ssl::stream as it is destroyed + boost::asio::post(ioContext, [this] + { + onAvailableConnection.disconnect(); + onUnavailableConnection.disconnect(); + + for (auto& [id, entry] : signals) + { + entry->onSubscribed.disconnect(); + entry->onMetadataChanged.disconnect(); + entry->onDataReceived.disconnect(); + entry->onUnsubscribed.disconnect(); + } + + if (wsConnection) + wsConnection->close(); + }); + thread.join(); - wsConnection->close(); } PropertyObjectPtr WsStreaming::createDefaultConfig() { auto obj = PropertyObject(); - obj.addProperty(IntProperty("Port", 7414)); + obj.addProperty(IntProperty(PROPERTY_WS_STREAMING_PORT_CLIENT, DEFAULT_WS_STREAMING_PORT)); return obj; } +PropertyObjectPtr WsStreaming::createDefaultSecureConfig() +{ + constexpr Int minPortValue = 0; + constexpr Int maxPortValue = 65535; + + auto defaultConfig = PropertyObject(); + + { + auto builder = IntPropertyBuilder(PROPERTY_WSS_STREAMING_PORT_CLIENT, DEFAULT_WSS_STREAMING_PORT) + .setMinValue(minPortValue) + .setMaxValue(maxPortValue); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = BoolPropertyBuilder(PROPERTY_ENABLE_MTLS_CLIENT, DEFAULT_ENABLE_MTLS); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = StringPropertyBuilder(PROPERTY_WSS_CERT_FILE_PATH_CLIENT, DEFAULT_WSS_CERT_FILE_PATH) + .setVisible(EvalValue(std::string("$") + PROPERTY_ENABLE_MTLS_CLIENT + " == 1")); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = StringPropertyBuilder(PROPERTY_WSS_KEY_FILE_PATH_CLIENT, DEFAULT_WSS_KEY_FILE_PATH) + .setVisible(EvalValue(std::string("$") + PROPERTY_ENABLE_MTLS_CLIENT + " == 1")); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = StringPropertyBuilder(PROPERTY_WSS_CA_CERT_FILE_PATH_CLIENT, DEFAULT_WSS_CA_CERT_FILE_PATH); + defaultConfig.addProperty(builder.build()); + } + + return defaultConfig; +} + void WsStreaming::onSetActive(bool active) { } @@ -179,9 +316,9 @@ void WsStreaming::onConnected( LOG_I("Connected to remote peer"); wsConnection = connection; - wsConnection->on_available.connect( + onAvailableConnection = wsConnection->on_available.connect( std::bind(&WsStreaming::onRemoteSignalAvailable, this, _1)); - wsConnection->on_unavailable.connect( + onUnavailableConnection = wsConnection->on_unavailable.connect( std::bind(&WsStreaming::onRemoteSignalUnavailable, this, _1)); } diff --git a/shared/libraries/websocket_streaming/src/ws_streaming_device.cpp b/shared/libraries/websocket_streaming/src/ws_streaming_device.cpp index 0705742..cf693fd 100644 --- a/shared/libraries/websocket_streaming/src/ws_streaming_device.cpp +++ b/shared/libraries/websocket_streaming/src/ws_streaming_device.cpp @@ -15,6 +15,7 @@ */ #include +#include "websocket_streaming/constants.h" #include #include @@ -31,7 +32,7 @@ BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING DeviceTypePtr WsStreamingDevice::createOldType() { return DeviceTypeBuilder() - .setId("OpenDAQLTStreamingOld") + .setId("OpenDAQLTStreamingOld") .setName("Streaming LT enabled pseudo-device") .setDescription("Exposes signals from devices streamed using the WebSocket Streaming Protocol") .setDefaultConfig(createDefaultConfig()) @@ -42,7 +43,7 @@ DeviceTypePtr WsStreamingDevice::createOldType() DeviceTypePtr WsStreamingDevice::createNewType() { return DeviceTypeBuilder() - .setId("OpenDAQLTStreaming") + .setId("OpenDAQLTStreaming") .setName("Streaming LT enabled pseudo-device") .setDescription("Exposes signals from devices streamed using the WebSocket Streaming Protocol") .setDefaultConfig(createDefaultConfig()) @@ -50,12 +51,35 @@ DeviceTypePtr WsStreamingDevice::createNewType() .build(); } +DeviceTypePtr WsStreamingDevice::createNewSecureType() +{ + return DeviceTypeBuilder() + .setId("OpenDAQLTStreamingSecure") + .setName("Secure streaming LT enabled pseudo-device") + .setDescription("Exposes signals from devices streamed using the WebSocket Streaming Protocol and TLS encryption") + .setDefaultConfig(createDefaultSecureConfig()) + .setConnectionStringPrefix("daq.lts") + .build(); +} + +DeviceTypePtr WsStreamingDevice::createOldSecureType() +{ + return DeviceTypeBuilder() + .setId("OpenDAQLTStreamingOldSecure") + .setName("Secure streaming LT enabled pseudo-device") + .setDescription("Exposes signals from devices streamed using the WebSocket Streaming Protocol and TLS encryption") + .setDefaultConfig(createDefaultSecureConfig()) + .setConnectionStringPrefix("daq.wss") + .build(); +} + WsStreamingDevice::WsStreamingDevice( const ContextPtr& context, const ComponentPtr& parent, const StringPtr& localId, const StringPtr& connectionString, - const DeviceTypePtr& type) + const DeviceTypePtr& type, + const PropertyObjectPtr& config) : Device(context, parent, localId) , connectionString(connectionString) , deviceType(type) @@ -64,7 +88,7 @@ WsStreamingDevice::WsStreamingDevice( DAQ_THROW_EXCEPTION(ArgumentNullException, "connectionString cannot be null"); name = "WebsocketClientPseudoDevice"; - streaming = createWithImplementation(connectionString, context); + streaming = createWithImplementation(connectionString, context, config); streaming.setActive(true); auto& wsStreaming = *reinterpret_cast(streaming.getObject()); @@ -75,9 +99,12 @@ WsStreamingDevice::WsStreamingDevice( PropertyObjectPtr WsStreamingDevice::createDefaultConfig() { - auto obj = PropertyObject(); - obj.addProperty(IntProperty("Port", 7414)); - return obj; + return WsStreaming::createDefaultConfig(); +} + +PropertyObjectPtr WsStreamingDevice::createDefaultSecureConfig() +{ + return WsStreaming::createDefaultSecureConfig(); } void WsStreamingDevice::removed() @@ -116,8 +143,8 @@ void WsStreamingDevice::onSignalAvailable( if (s.getLocalId() == localId) openDaqDomainSignal = s; if (!openDaqDomainSignal.assigned()) - throw NotFoundException( - "Streaming signal '" + signal->id() + "' refers to unregistered domain signal '" + domainSignal->id() + "'"); + DAQ_THROW_EXCEPTION(NotFoundException, + "Streaming signal '{}' refers to unregistered domain signal '{}'", signal->id(), domainSignal->id()); } auto openDaqSignal = createWithImplementation( @@ -141,6 +168,9 @@ void WsStreamingDevice::onSignalAvailable( void WsStreamingDevice::onSignalUnavailable(wss::remote_signal_ptr signal) { auto lock = getRecursiveConfigLock2(); + if (this->objPtr.template asPtr(true).isRemoved() || !streaming.assigned()) + return; + auto it = streamingSignals.find(signal->id()); if (it == streamingSignals.end()) return; diff --git a/shared/libraries/websocket_streaming/src/ws_streaming_server.cpp b/shared/libraries/websocket_streaming/src/ws_streaming_server.cpp index f9c36ac..10ebf53 100644 --- a/shared/libraries/websocket_streaming/src/ws_streaming_server.cpp +++ b/shared/libraries/websocket_streaming/src/ws_streaming_server.cpp @@ -14,11 +14,11 @@ * limitations under the License. */ -#include #include #include #include +#include #include #include @@ -31,6 +31,7 @@ #include +#include #include #include #include @@ -46,13 +47,69 @@ PropertyObjectPtr WsStreamingServer::createDefaultConfig(const ContextPtr& conte auto defaultConfig = PropertyObject(); - const auto websocketPortProp = - IntPropertyBuilder("WebsocketStreamingPort", 7414).setMinValue(minPortValue).setMaxValue(maxPortValue).build(); - defaultConfig.addProperty(websocketPortProp); + { + auto builder = BoolPropertyBuilder(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER, DEFAULT_ENABLE_WS_STREAMING_PORT); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = BoolPropertyBuilder(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER, DEFAULT_ENABLE_WS_CONTROL_PORT); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = IntPropertyBuilder(PROPERTY_WS_STREAMING_PORT_SERVER, DEFAULT_WS_STREAMING_PORT) + .setMinValue(minPortValue) + .setMaxValue(maxPortValue) + .setVisible(EvalValue(std::string("$") + PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER + " == 1")); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = IntPropertyBuilder(PROPERTY_WS_CONTROL_PORT_SERVER, DEFAULT_WS_CONTROL_PORT) + .setMinValue(minPortValue) + .setMaxValue(maxPortValue) + .setVisible(EvalValue(std::string("$") + PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER + " == 1")); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = BoolPropertyBuilder(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, DEFAULT_ENABLE_WSS_STREAMING_PORT); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = BoolPropertyBuilder(PROPERTY_ENABLE_MTLS_SERVER, DEFAULT_ENABLE_MTLS) + .setVisible(EvalValue(std::string("$") + PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER + " == 1")); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = IntPropertyBuilder(PROPERTY_WSS_STREAMING_PORT_SERVER, DEFAULT_WSS_STREAMING_PORT) + .setMinValue(minPortValue) + .setMaxValue(maxPortValue) + .setVisible(EvalValue(std::string("$") + PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER + " == 1")); + defaultConfig.addProperty(builder.build()); + } - const auto websocketControlPortProp = - IntPropertyBuilder("WebsocketControlPort", 7438).setMinValue(minPortValue).setMaxValue(maxPortValue).build(); - defaultConfig.addProperty(websocketControlPortProp); + { + auto builder = StringPropertyBuilder(PROPERTY_WSS_CERT_FILE_PATH_SERVER, DEFAULT_WSS_CERT_FILE_PATH) + .setVisible(EvalValue(std::string("$") + PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER + " == 1")); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = StringPropertyBuilder(PROPERTY_WSS_KEY_FILE_PATH_SERVER, DEFAULT_WSS_KEY_FILE_PATH) + .setVisible(EvalValue(std::string("$") + PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER + " == 1")); + defaultConfig.addProperty(builder.build()); + } + + { + auto builder = StringPropertyBuilder(PROPERTY_WSS_CA_CERT_FILE_PATH_SERVER, DEFAULT_WSS_CA_CERT_FILE_PATH) + .setVisible(EvalValue(std::string("($") + PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER + " == 1) && ($" + + PROPERTY_ENABLE_MTLS_SERVER + " == 1)")); + defaultConfig.addProperty(builder.build()); + } defaultConfig.addProperty(StringProperty("Path", "/")); @@ -104,10 +161,45 @@ WsStreamingServer::WsStreamingServer( , _ioc{1} , _server{_ioc.get_executor()} { - _port = config.getPropertyValue("WebsocketStreamingPort"); + _port = config.getPropertyValue(PROPERTY_WS_STREAMING_PORT_SERVER); + + if (config.getPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER).asPtr().getValue(False) == True) + { + _server.add_listener(config.getPropertyValue(PROPERTY_WS_STREAMING_PORT_SERVER)); + } + if (config.getPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER).asPtr().getValue(False) == True) + { + _server.add_listener(config.getPropertyValue(PROPERTY_WS_CONTROL_PORT_SERVER), true); + } - _server.add_listener(config.getPropertyValue("WebsocketStreamingPort")); - _server.add_listener(config.getPropertyValue("WebsocketControlPort"), true); + if (config.getPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER).asPtr().getValue(False) == True) + { + std::string ca_cert; + if (config.getPropertyValue(PROPERTY_ENABLE_MTLS_SERVER).asPtr().getValue(False) == True) + { + ca_cert = config.getPropertyValue(PROPERTY_WSS_CA_CERT_FILE_PATH_SERVER).asPtr().toStdString(); + if (ca_cert.empty()) + { + DAQ_THROW_EXCEPTION(InvalidParameterException, "Mutual TLS is enabled but no CA certificate file path is configured"); + } + } + std::string server_cert = config.getPropertyValue(PROPERTY_WSS_CERT_FILE_PATH_SERVER).asPtr().toStdString(); + std::string server_key = config.getPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_SERVER).asPtr().toStdString(); + + if (server_cert.empty() || server_key.empty()) + { + DAQ_THROW_EXCEPTION(InvalidParameterException, "TLS certificate or key file path is not configured"); + } + + try + { + _server.add_tls_listener(config.getPropertyValue(PROPERTY_WSS_STREAMING_PORT_SERVER), server_cert, server_key, ca_cert); + } + catch (const std::exception& e) + { + DAQ_THROW_EXCEPTION(InvalidParameterException, "Cannot load the TLS secrets: {}", e.what()); + } + } _onClientConnected = _server.on_client_connected.connect( std::bind(&WsStreamingServer::onClientConnected, this, _1)); @@ -141,7 +233,7 @@ PropertyObjectPtr WsStreamingServer::getDiscoveryConfig() discoveryConfig.addProperty(StringProperty("ServiceName", "_streaming-lt._tcp.local.")); discoveryConfig.addProperty(StringProperty("ServiceCap", "LT")); discoveryConfig.addProperty(StringProperty("Path", config.getPropertyValue("Path"))); - discoveryConfig.addProperty(IntProperty("Port", config.getPropertyValue("WebsocketStreamingPort"))); + discoveryConfig.addProperty(IntProperty("Port", config.getPropertyValue(PROPERTY_WS_STREAMING_PORT_SERVER))); discoveryConfig.addProperty(StringProperty("ProtocolVersion", "")); return discoveryConfig; } @@ -156,9 +248,12 @@ void WsStreamingServer::onStopServer() // openDAQ can (but probably should not) call onStopServer() more than once. if (_thread.joinable()) { - _ioc.stop(); + // Close the ws-streaming server on its own I/O thread (it is not thread-safe): this cancels + // the listeners' pending accepts and closes every client connection, letting _ioc.run() + // drain and return so the thread can be joined. + + boost::asio::post(_ioc, [this] { _server.close(); }); _thread.join(); - _server.close(); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..b6af164 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.10) +opendaq_set_cmake_folder_context(TARGET_FOLDER_NAME) + +add_subdirectory(integration) diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt new file mode 100644 index 0000000..94f11cd --- /dev/null +++ b/tests/integration/CMakeLists.txt @@ -0,0 +1,30 @@ +set(TEST_APP test_lt_streaming_tls_integration) + +set(TEST_SOURCES test_lt_streaming_tls.cpp + test_app.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES} +) + +target_link_libraries(${TEST_APP} PRIVATE daq::opendaq_test_utils gtest + daq::opendaq + daq::opendaq_mocks + daq::websocket_streaming +) + +add_dependencies(${TEST_APP} daq::ws_stream_cl_module daq::ws_stream_srv_module) + +# Copy the pre-generated TLS fixtures next to the test binary +add_custom_command(TARGET ${TEST_APP} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/secrets + $/secrets) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $) + +if (COMMAND setup_target_for_coverage AND OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${TEST_APP}coverage ${TEST_APP} ${TEST_APP}coverage) +endif() \ No newline at end of file diff --git a/tests/integration/secrets/ca.crt b/tests/integration/secrets/ca.crt new file mode 100644 index 0000000..14650c3 --- /dev/null +++ b/tests/integration/secrets/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHzCCAgegAwIBAgIUVpDTDw99WWrq7sJIMAziliBPTt4wDQYJKoZIhvcNAQEL +BQAwHzEdMBsGA1UEAwwUTFQgU3RyZWFtaW5nIFRlc3QgQ0EwHhcNMjYwNzIwMTMy +NTM5WhcNMzYwNzE3MTMyNTM5WjAfMR0wGwYDVQQDDBRMVCBTdHJlYW1pbmcgVGVz +dCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJYHc4M9L1xyzG2Z +MAqRRigidxQ7mSdBBPRVhfFlS25jCadnfjmDYY90CR+tpFkF5puL7XzgD4Yud6sd +6EWNX5y/KRcyvPJxgOQ/SUQAMEqZrnjh6cMoOPagvDFw77E/jrz9h6tT/WkKS8On +CqsEEUo02Txv+4JAfwAc4yP944Hi61bqVaHm8/FJ2+iW5/QBXh7NNbZ7lJLUqMPT +7GGGKyFWPVKZMlpCLUwASYfR5EJrTmxWr6j9b+IzrPULpNIXeKzrM3+hiodZ1FSK +LnTjwlHGrUWuOllMG1ioIe2tLH+bxL9RXEBFQ8VR1rI1/8kGQglA2KN/NR7T2JdV +Zft5zK8CAwEAAaNTMFEwHQYDVR0OBBYEFC/ftORjILzi/M85c9powcoIMJ7PMB8G +A1UdIwQYMBaAFC/ftORjILzi/M85c9powcoIMJ7PMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAIBh6nxwDDGE4Cb5qztvL65vkz9cYkhZXNdFicN+ +3coI+iXqtXcg2OuGE/0CGY/8M0YzNP5hVqJhU/KiXwbPHU1IIxPBW+ZVIz3lsDhB +T0rf6IDHHzWP0IqXYFcL2oRi+tTMjsK83XQFYP9zYBX56X9Z3fxY2dvAn4F5/jYA +ELQwARf1q2KsRMf44lN78zc848RhP2a4e+4YqpDhfEfrM3zTyZpPpqrsP6gOYNz2 +ddyFl7A/a2NSdPQHgB1nX2g6b1owmaDYup5AN4/SQT0Pzn1zrPsToBJpC1txzZOO +C15bN+HnucVFU3rFUtM+O5NuAPSEPa18bWsn2Sbq4Tdj4TY= +-----END CERTIFICATE----- diff --git a/tests/integration/secrets/ca.key b/tests/integration/secrets/ca.key new file mode 100644 index 0000000..246c311 --- /dev/null +++ b/tests/integration/secrets/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCWB3ODPS9ccsxt +mTAKkUYoIncUO5knQQT0VYXxZUtuYwmnZ345g2GPdAkfraRZBeabi+184A+GLner +HehFjV+cvykXMrzycYDkP0lEADBKma544enDKDj2oLwxcO+xP468/YerU/1pCkvD +pwqrBBFKNNk8b/uCQH8AHOMj/eOB4utW6lWh5vPxSdvoluf0AV4ezTW2e5SS1KjD +0+xhhishVj1SmTJaQi1MAEmH0eRCa05sVq+o/W/iM6z1C6TSF3is6zN/oYqHWdRU +ii5048JRxq1FrjpZTBtYqCHtrSx/m8S/UVxARUPFUdayNf/JBkIJQNijfzUe09iX +VWX7ecyvAgMBAAECggEAFh/OLIXa1J7HP8hN2dkXh/ahKdYqHSxM7z0nbfQ6nfIz +68Bq0E9sFSwO5mGEcW5Z1glaAcbG80op8o2MAam59lMIW7frPjN3xvEAPGtss6ub +G49xGgr174S/i+j0J+bQBVW76ouNvK48gwNAovyMEi3d9TgsVkZtlBYudtHLeY1D +xogI8kFUB1040uS93l/9M88H1rzxPkBqRn3BOKzCrqwcnbHqEJ4Yy1GRMKvFDgaS +v4Eqjv1oBUYwvJ5MqFRUts09uafsQO4yBcIgAoLdOgtGUrncOW/lpOfxuV0+4/01 +f9kZ6TUGN5aW6X3W8MUKeSKyhd6Gy6RYrJvFPxYgAQKBgQDI7qiQCJpFyokSskUH +doVem16OW4Jh8mEeTa9BMzRuBNmCUVAKOZGDjUOIyNK8WV/cOmzEvQtro7tzXgmO +W+J6XoapDeNqfZoLpcvYx2xNSNOr0OjOcCC8DuJBYBoFMJFlKm1hULBa0mp1OmrH +j/nwzLhhusQ4ZOzLm+xV5F3gAQKBgQC/JXG3jViKKVDaX7GFSVhbAr5109+mYJHU +eTBKpQ4remvt1CXyRnw0XNa5j7PDWfZu6+ttBEoXWdBJTYBFx8d86y42NoX1UNHW +U1O8sEO3691XaXSlQ6NqtcPY6oZCkpjoyqIphXTY7vZOnny1fChgK+FiAfxvuO6U +D/eTbM2srwKBgAvviVnM+QUOS+gtaMZRtwhwqERCkVMqJ7S6Hp/737BtX1JLbDq/ +QrKS0KEudL41fOlDGzTwmd4SAs+OlXi8Yo6sUMl4BrtLrLvmZF6DMh7BJbm+FiY1 +KOyi4bPKdqE6Sli9FXRDa8ZpW2ONTcx+8/Tm5EkB5CgJbruFjptnjmABAoGAKiUl +Z6VAqmXlr+B9kbfURWzbdbtbiqGsRb7ayPqPn0KE3ZFzNHIaHKmDprJqnl4NmfNm +m0jg1Tk2VZNC/x/HemGYdNecvbQuMCV7RxVOORVGXmmgEHhTZcnh/sfnBnKwSFO1 +YZZNZy95G2iScqmHdXz3Wta5RCDwabEDk3vf9X0CgYAMVNP1zSs2/x8YUKKX2Um0 +GAlfwWDQchReCS0tSd73Ma/bnVUlIizwkrEtXf5Ch8ilrM3DGA67NmX6odliePCD +EfxO8i3EM+g+VJUMKosuzdjZjdCFQv1XL9DtXfIOegmjsz0vB/Ll4TbxMbvfXWxc +gGzzazmWL1XhQ5PIK8jAgQ== +-----END PRIVATE KEY----- diff --git a/tests/integration/secrets/client.crt b/tests/integration/secrets/client.crt new file mode 100644 index 0000000..f945070 --- /dev/null +++ b/tests/integration/secrets/client.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJzCCAg+gAwIBAgIUA4i6ymXsjOtZJ/KvqTPcok+3iL0wDQYJKoZIhvcNAQEL +BQAwHzEdMBsGA1UEAwwUTFQgU3RyZWFtaW5nIFRlc3QgQ0EwHhcNMjYwNzIwMTMy +NTM5WhcNMzYwNzE3MTMyNTM5WjAjMSEwHwYDVQQDDBhsdC1zdHJlYW1pbmctdGVz +dC1jbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCa1tPHF31z +jkNj13kmoC1+Yz0UpORyOhsmTbNGog2HFLjfUIyjqWLnu3S8mFasOdDXb7WJOyrT +EpmJ9zYv15DLqf4uKPqY6/wgucyB2c1vfCzm+eB6OvL06T9kiSAEUXx0fPQcL7tI +w9I30y/iJAe4OD//VoFpfVAJZyg47RpqUsJROOyV1thHxYGHE2b5Bb0eGrv4MUbJ +/FZJXUOdCE2gakZI80KwWY2PlBjYFn8B5TXg3Kt11Pxv/yKKHHABMgmgW6qJX6vF +1xa8HY5CTFgq15q2OTC/hAapPJ3GrDBrBEYNktuWqonS6bfTIOnCYLaLFdCjW3v3 +g4qk2PfevT5LAgMBAAGjVzBVMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW +BBQdd0Lt8vwcSRQXu2w17styvejl8TAfBgNVHSMEGDAWgBQv37TkYyC84vzPOXPa +aMHKCDCezzANBgkqhkiG9w0BAQsFAAOCAQEAZl/qkshqf6ihtnwoHpYeVzONQs7K +XHl3NjDpGrDcHUr5qkd6sGlQCPTGvhC8uQE5aucs+e7C9hu8zdGA7uRcrVzdPsHZ +1RBfhJ2WpBKvnTqsBBUL1XK0r3VNjJeCgF4yDlvfjr4SxdVXQAIv1h8q83soAWiM +ahd9IagwKMa+RF3iRlARzHhhuyCMM5GuFDwcsQ89hWAy3+EZ/m/LAAcjpdj+UjsM +hhf925z+mKebqzh1Mh0+zFs+faKsvzUWcWuPx8bM3nEhN+wd4hqeR6EufX13gBHb +hIhwwakY7KfURgFu/QYpHXeQ5438EiAHMG1fqdO0v6Z4YIIjAxWdzuyr2A== +-----END CERTIFICATE----- diff --git a/tests/integration/secrets/client.key b/tests/integration/secrets/client.key new file mode 100644 index 0000000..52598fc --- /dev/null +++ b/tests/integration/secrets/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa1tPHF31zjkNj +13kmoC1+Yz0UpORyOhsmTbNGog2HFLjfUIyjqWLnu3S8mFasOdDXb7WJOyrTEpmJ +9zYv15DLqf4uKPqY6/wgucyB2c1vfCzm+eB6OvL06T9kiSAEUXx0fPQcL7tIw9I3 +0y/iJAe4OD//VoFpfVAJZyg47RpqUsJROOyV1thHxYGHE2b5Bb0eGrv4MUbJ/FZJ +XUOdCE2gakZI80KwWY2PlBjYFn8B5TXg3Kt11Pxv/yKKHHABMgmgW6qJX6vF1xa8 +HY5CTFgq15q2OTC/hAapPJ3GrDBrBEYNktuWqonS6bfTIOnCYLaLFdCjW3v3g4qk +2PfevT5LAgMBAAECggEABIj0FqvihYoJ8E//0j8f0beO5icWutkzfhkJjyiTGaeX +lcPgUheZ0bcoFKin9orWsF8I8qP/7I+/bmhbbncEfqXC4NyPMd2ImKIcvN2sMu6j +26IdY6pIdpX+SQ2n5iMlX1c+4kWFv8krZfYbGfx7oaoqfjbxVHHjZ9IHxsDrBr3d +UOTdH/nDvmD1x9R48xdO4OEE/goajivBSzUpNJsiiGEtRvL/6DCbGDG+LwbGiOdJ +XbcBswk5yWz95dPdPiCJpyZnFuFj+Lfls4vzCR1KNcqokFuWmQzsX+CmazZmlz5p +i/U/0nL7eKEbolUNfIPcI+o15wR2pdozZODX4NmAwQKBgQDInzkSHRSQsw8ATgzs +1ImZOmwcaOiaIhtyZRsbHUaJ9JLMcBDo7ZgsgCNwRrNao8VuoZaPWyJGDjXwU+Ul +iku85cV2ed5xr49yY7YizTAN8/PqAK4FLbQrc63V5q6MdfafRYeWyWLWPj2WW12y +HBAjP5wfcEc/vTb66eIqIJfGvwKBgQDFlGcdeMPGdZIHaZd66XuqBGp6snjSY78+ +3TWMamhlhe6fGlvaOXm9NdiDMVbgjTgECYU1uTbe24LwHF+15BjU/HJNvtl5wJ7G +UDeYcrhDZtKNvL49O0Yie3MNtU9Fhmrw3kFSbZc3+LY7U9It1oESAknKyP3tUbru +veJlGMvXdQKBgDgcoH+6dngKPNXe/qp2v42F05sWlIgXpgmW0cABrisDptNbPL+J +RJidtku+2octaxkIRO4PlUR20QtIy7x2M7y/nz0F1PuDGOHrlZOiRCCC7OO0EPgN +0mic0DAVdxh+Hd5qOgQoi2ZUvjEBmPEp/DF/pxD7/FGNgIUpSUJzH0BNAoGBAK// +M6MTEgFUdVHZAzBfjuKxsE+/btSqOY876Rk9J+VXWmmrRyqqjAC5WDX021iHMuCV +wm1KoM//ZTGRdMhHeL8waHlMidhkdopcpklL10ti6ieLe0J77EuG/8O0P/rEc7o/ +oJxWNp+LEnjif5SJQMvT8fuK8nZVmVhVGLfJULqJAoGBAKT4XxhFqUNXSzJ5By8Y +uCJBqjJAC+LFsE2wmWaflBEVI45eKPGMVXZ6hprmEXVER2OUSHq8cakJLLC2X6Wv +K+2BZIab9IXSLdzg4oUatyMsnvxg8gdLCxF9sNVbpGeyn6IU+56HVl999PEDmamg +bCDMa1aIihKiWhGFmDEf0Mni +-----END PRIVATE KEY----- diff --git a/tests/integration/secrets/other-ca.crt b/tests/integration/secrets/other-ca.crt new file mode 100644 index 0000000..b7f1529 --- /dev/null +++ b/tests/integration/secrets/other-ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDITCCAgmgAwIBAgIUfekNd0wn4/GI5wn8aohycRgT3XMwDQYJKoZIhvcNAQEL +BQAwIDEeMBwGA1UEAwwVTFQgU3RyZWFtaW5nIE90aGVyIENBMB4XDTI2MDcyMDEz +MjUzOVoXDTM2MDcxNzEzMjUzOVowIDEeMBwGA1UEAwwVTFQgU3RyZWFtaW5nIE90 +aGVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuZ+piepubTV/ +jkRBwQLVJlIXFIXk9yP8+OKWQ9X+/h7a8p632vLCRAGNdIPi0TmHmeIwEoi9BEm2 +K3VfgCuNurxiFHcnS2pVDPkgt6CUrgXFcAm9R/HOVle3HmxfHOU7BWebdgt1O3Kt +4diNGTZi024y1NkuVGqZ1b6yCCDF10XBaV1d7o4NiSNcOomNoEEjRFoSZtB3fw5z +oPoPmEv3lSo/XFlEset01ApojOXUzLcx18j5Pg+HPSICKF5lcqqizd19K46xYXzE +O+ok9bDTrlJmgYqgCDSXz7firgrowOlHCWjTCkbyyYfdpuC7LSOFct2BP1YJpVt3 +Or0XU78BJQIDAQABo1MwUTAdBgNVHQ4EFgQUvndRFDiy2XjKpdIheUjWEGPBc3Uw +HwYDVR0jBBgwFoAUvndRFDiy2XjKpdIheUjWEGPBc3UwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEAaZGq/w5GO/S7iRjt3rqlgnfnnt6bA931ehhL ++QPTRF/zSP7ecxjZ5x93Kgt/9Fehw89vwYj0fz3RF1SQrlaA8RGWzTMsPB5Dhsok +x36af3vpbWsdIToj8sIPJeICz0q+kGGBzOSwfs+8zdebVFSOK+fGJU3cEtOAilOj +rBMcoEe5tQUBzFpB/0VnntxlYOvvPXJRM0QpKUzb3Fp9Y1rZ2409DKmuASPbtLdM +UwgBviISiDpBRZj3iduYx3D4ydZX7QhzA1drlUtDAWfMXvZ72547qiL71vmmc/o7 +W/A8/o7fw/zUwOVxwV6lltYrtpVvWZzNjfCrtIUAOPFijnkUNw== +-----END CERTIFICATE----- diff --git a/tests/integration/secrets/other-ca.key b/tests/integration/secrets/other-ca.key new file mode 100644 index 0000000..ca1de52 --- /dev/null +++ b/tests/integration/secrets/other-ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC5n6mJ6m5tNX+O +REHBAtUmUhcUheT3I/z44pZD1f7+Htrynrfa8sJEAY10g+LROYeZ4jASiL0ESbYr +dV+AK426vGIUdydLalUM+SC3oJSuBcVwCb1H8c5WV7cebF8c5TsFZ5t2C3U7cq3h +2I0ZNmLTbjLU2S5UapnVvrIIIMXXRcFpXV3ujg2JI1w6iY2gQSNEWhJm0Hd/DnOg ++g+YS/eVKj9cWUSx63TUCmiM5dTMtzHXyPk+D4c9IgIoXmVyqqLN3X0rjrFhfMQ7 +6iT1sNOuUmaBiqAINJfPt+KuCujA6UcJaNMKRvLJh92m4LstI4Vy3YE/VgmlW3c6 +vRdTvwElAgMBAAECggEAI91X/gg4Hsyr4RqKE9dOxLEuuW3nX+LpsVLiUqgJtSLU +DfWhs2GdLFOad601yVQSwaZEwIf/8VCmrH3onlSL9U0JrEebZYRde3y1GBWkNH1z ++OJA2qI7Sk53WJ76v5QA09oBXQz4sSxGsXDMTldGP/C2TI7L106je3t1XYGriByj +KagQgyO+5YDnefADvX1WG3VblbLU1/CC7RI2URXQoxs616f8IEotALb224KfY67W +c6zCF5Jlsyc9Cq1nr58uw9uB0ucOHGAPqZQle9MnCA4X/HO9Z99sApKSGBqHHui7 +j5ombWcQq7irCDxgjcQEu2EsYdyBVwGJp4ODcTWe/wKBgQDtFaEvyN0AdRYJDJ69 +YENiicMru7zaA60XgraNyi8UrEO33Y5hhMmzBE96+HLfvnC0dP9eTLMqnaXCPpr8 +dgzY7SsIaNTFtrEaEkDYtOuA66+cSuB/7ADNUgAtURLVeGJ7+X7vZrMJVbXkIKwj +PP42lTvYsb1cQSO3we0hc2HQQwKBgQDIbvaIopu7OVtVU1l6jJzexABSmLYE1jgR +qByZ8tMW1D3B3+tZuWmp5ZC+Up9Q1ee/1ukmFLNcZkrN+x7vn14GoSi7p2znKP9w +rCBqjI1YRn/5nzP4EVljHurqScfNYjm8baLdnPoeTe+9R2w+27mUtUJ7Eukl0JrC +ISMdVfnmdwKBgQCmUj4535fq8jj7MDdMc3PSdQLOc24RI/MSQ0j+P5C32AHJw0xX +bWhyhducUR+Fh3xZnJpp9TTb+KyIkJL5+XcZ14gQ5joeWgP0XknPvVxZnP4djwnf +tBj78ULeh7nGuCWfA3DhUhLXwaFQ4Prh3bAZaqgJZOeLCMed6hYJGYDNMQKBgQCo +aOPo5ohnhcgCJ6h1WhSs8I8812eJJN2zqEdv6yIGeorxjc3Yc/webu9xm0aVy0WQ +YWlBP/980c5kyG2z9Xoxo9Pv6LuOd1o6N8Z9AF/7HjGFcoleFGXA7UeBWEDzh2bv +tkc0YiOetumLZqnDHygNlUeo1bjktyRxnRo+GfGB/QKBgQCXdzRbhjr6/dEo0snV +NeY4oBsUz8Nkbxx9Sv4gVcsrTGJr580+TI0K+Or+vPhWQ63J6+1xMjqLZS92H20r +FMHi4jEEEjzggYeaD3UaMjajIj3yEiAaGox7CLQbEA+AN9HjLGZ+c1g4skk63Q3A +dqZhaB5Zn6e3UIXdI2Ge2Aqf6A== +-----END PRIVATE KEY----- diff --git a/tests/integration/secrets/server.crt b/tests/integration/secrets/server.crt new file mode 100644 index 0000000..adb606e --- /dev/null +++ b/tests/integration/secrets/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQzCCAiugAwIBAgIUA4i6ymXsjOtZJ/KvqTPcok+3iLwwDQYJKoZIhvcNAQEL +BQAwHzEdMBsGA1UEAwwUTFQgU3RyZWFtaW5nIFRlc3QgQ0EwHhcNMjYwNzIwMTMy +NTM5WhcNMzYwNzE3MTMyNTM5WjAjMSEwHwYDVQQDDBhsdC1zdHJlYW1pbmctdGVz +dC1zZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9/4BBFwGC +42iCoDWQpZWmIfI6iPd3CDRah08DP+W1/Qsz/a9bwKx+rMj0r0Y6WyAmq9jQU7+o +c5MK3Mp3swGCk93GPJIfRflGyC0uUJClq/trq5hUf/edcgvc656UAZwIJZTQNV3P +H6De2D9L0yDcr9ZMoISCQK8EdvwCwzAx8/nmlSZJChQxyKXjsj5MWrc1gR27jrrp +iuaMBqCqdgdCKKIncvEDml6c/ZrRnOr4fIIpHtHxJIMgOBqJ20ZGfYM0kovUtUT8 +XHEO4sO4rWsUwJ7LFLaToiGHxkihpV09kT4b5m2qGTPmdyrzlD+6Kl7L0EZC5MHh +6MIi5hOxcC6bAgMBAAGjczBxMBoGA1UdEQQTMBGHBH8AAAGCCWxvY2FsaG9zdDAT +BgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUPX28hQSKyp/4frN50hVs9rr6 +8GQwHwYDVR0jBBgwFoAUL9+05GMgvOL8zzlz2mjByggwns8wDQYJKoZIhvcNAQEL +BQADggEBADdRhjll7rnBLrYDPrShpeUaairJTQ53rj3WDa2LwXicWpSpsiv046G2 +2QZa6E/DaJwPd7LD4+WkRKxLz8m3oZg9RmjXBtAJmDBIrRk/MWzv82jpNTmnrtVW +LKy+TgNz3y2nsJvodjwiSbThilwyR0z8o499OsRqMSkAPktqEvFW2l9m8FKQf3Xd +Yb2bLL9osXM7xRz04RHKvpJkSfFwUjaqujjQ4w4eSzw0Gp9dpFD8X5uJX+HtUJ9T +t0sPyCseD+OnoDWV5aMdMEJQZs++/j3vduRiDA/vJtw94VYBpora3tZrAavgW6WW +VW58it7Xn90gwaIbZWQEQR/DzqNOEhw= +-----END CERTIFICATE----- diff --git a/tests/integration/secrets/server.key b/tests/integration/secrets/server.key new file mode 100644 index 0000000..61236cc --- /dev/null +++ b/tests/integration/secrets/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9/4BBFwGC42iC +oDWQpZWmIfI6iPd3CDRah08DP+W1/Qsz/a9bwKx+rMj0r0Y6WyAmq9jQU7+oc5MK +3Mp3swGCk93GPJIfRflGyC0uUJClq/trq5hUf/edcgvc656UAZwIJZTQNV3PH6De +2D9L0yDcr9ZMoISCQK8EdvwCwzAx8/nmlSZJChQxyKXjsj5MWrc1gR27jrrpiuaM +BqCqdgdCKKIncvEDml6c/ZrRnOr4fIIpHtHxJIMgOBqJ20ZGfYM0kovUtUT8XHEO +4sO4rWsUwJ7LFLaToiGHxkihpV09kT4b5m2qGTPmdyrzlD+6Kl7L0EZC5MHh6MIi +5hOxcC6bAgMBAAECggEAGrBnXb1TkwVKyv6iR51a9ilSk3JmAKyAXi8fpD83j2bq +SfVayrK4CFtRJz3vyYa/b284V5tMEueZQi9dqY7VqXgH8S7U/9uedpjr79OVXhBY +kvlZvpTzPYSlsfTPI89OEb7ltpGoic3RlTwfOBGCTj3NBpqNwpY/f4sPP3wXqwxy +cpSZVe403YX2NwsCiSOUB8erUOXGwJLVqBN8+7CyziYV1KZyU2fyMbTIXmhHmYLB +dmp+gBb7VAtV3A5ioWLX/70msWvHbcFvwyM6ZKWw49423tvd0HHO+w5Yx0cL69oL +omGgP8zT3INIojjJu8QYu7jc89vU+wlNsDQjiTSJsQKBgQDsts0G4aJZZufoe46o +LPgj6oNQPumMv6N72sUPfN4897mku8u/D3j/qlSWfjuPV/b2fiS0u6G5IHt/9upi +q1QbJicjUkMX22kfl/21m0Wgej9qgjHjirIHpqPN9Zy9PtzpI4L16JtV7dFGHE37 +CTPt5/13qcn8lzVfKiryta/LpwKBgQDNelVjr38CthwzVxYjmvXejyEWmyXioKfJ +KJN1uggtGL5o9hLI5jgEhq6+Ra5GRr9qJ47ON9x1RHWj/XBJOQOpDhwIRzOyLMXI +PnkBqQ38Du8GbWe6RvXKNqKRNPcEW05KgKsHJksZYy0esfjoaLjqeKmwJ/XDQ/ab +KEmRgzrT7QKBgQC3veGCAOzbRqSm8hm280kd3/BT/rGdHjhRcGxcbEm5u3TvqRIr +WWBL3DRSqOOkjdxWU248mBSdtdgHaWtgcasUc9YXdcg8oYSFEJ7QNBmkyp+lFjCR +dlxg0fn+ajNjNoOVuIn0Rb0lwzcZm+jv/qG/fr9h/nqaRekY2IuD4IH3qwKBgQC2 ++p6509l+F6ALVB0B/ge/dqQXlnfpVfm0l+6g4tZPGn7Nx3w47VKrLSjFhhXIvU3O +AYYZPAOVZi6Ap7traVtKG6d487ckjtxeEsv9X4d1k/LlPV4msvREa9zfBFSIy6pv +jB9BkadE512RvRkUFdW970/k7+DaT1Z3GyxQaZVkjQKBgAEPfR2nlUqiZFoAoy0f +FV0qukXLcRziyzRyn84DvJd0QY4TRy4xJRNW4JwgL9rQufVbFPG9KhonCce+YaDU +Kl0XbUGEETJUE1+p8L1Ki10ezSiAbg+v9e1cT+fTfaTCqLnVYiyWj8qmsgn9gMSf +NO23ON3Zbt+pArQN2fqCOw4X +-----END PRIVATE KEY----- diff --git a/tests/integration/test_app.cpp b/tests/integration/test_app.cpp new file mode 100644 index 0000000..859a905 --- /dev/null +++ b/tests/integration/test_app.cpp @@ -0,0 +1,20 @@ +#include +#include + +#include +#include + +int main(int argc, char** args) +{ + { + daq::ModuleManager("."); + } + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new DaqMemCheckListener()); + + auto res = RUN_ALL_TESTS(); + + return res; +} diff --git a/tests/integration/test_lt_streaming_tls.cpp b/tests/integration/test_lt_streaming_tls.cpp new file mode 100644 index 0000000..81f125d --- /dev/null +++ b/tests/integration/test_lt_streaming_tls.cpp @@ -0,0 +1,325 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace daq; +using namespace daq::websocket_streaming; +using namespace std::chrono_literals; + +namespace +{ +constexpr const char* CA_CERT = "secrets/ca.crt"; +constexpr const char* OTHER_CA_CERT = "secrets/other-ca.crt"; +constexpr const char* SERVER_CERT = "secrets/server.crt"; +constexpr const char* SERVER_KEY = "secrets/server.key"; +constexpr const char* CLIENT_CERT = "secrets/client.crt"; +constexpr const char* CLIENT_KEY = "secrets/client.key"; + +constexpr const char* SERVER_TYPE_ID = "OpenDAQLTStreaming"; +constexpr const char* SECURE_DEVICE_TYPE_ID = "OpenDAQLTStreamingSecure"; +constexpr const char* SIGNAL_NAME = "ByteStep"; +} + +class LtStreamingTlsTest : public testing::Test +{ +protected: + InstancePtr createServerInstanceWithDevice() + { + const auto logger = Logger(); + const auto moduleManager = ModuleManager("."); + const auto context = Context(Scheduler(logger), logger, TypeManager(), moduleManager, AuthenticationProvider()); + + moduleManager.addModule(MockDeviceModule_Create(context)); + + auto instance = InstanceCustom(context, "server"); + instance.addDevice("daqmock://phys_device"); + return instance; + } + + PropertyObjectPtr baseServerConfig(const InstancePtr& instance) + { + return instance.getAvailableServerTypes().get(SERVER_TYPE_ID).createDefaultConfig(); + } + + PropertyObjectPtr insecureServerConfig(const InstancePtr& instance, int wsPort) + { + auto cfg = baseServerConfig(instance); + cfg.setPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER, True); + cfg.setPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER, False); + cfg.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, False); + cfg.setPropertyValue(PROPERTY_WS_STREAMING_PORT_SERVER, wsPort); + return cfg; + } + + PropertyObjectPtr secureServerConfig(const InstancePtr& instance, int wssPort, bool mtls) + { + auto cfg = baseServerConfig(instance); + cfg.setPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER, False); + cfg.setPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER, False); + cfg.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, True); + cfg.setPropertyValue(PROPERTY_WSS_STREAMING_PORT_SERVER, wssPort); + cfg.setPropertyValue(PROPERTY_ENABLE_MTLS_SERVER, mtls); + cfg.setPropertyValue(PROPERTY_WSS_CERT_FILE_PATH_SERVER, SERVER_CERT); + cfg.setPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_SERVER, SERVER_KEY); + if (mtls) + cfg.setPropertyValue(PROPERTY_WSS_CA_CERT_FILE_PATH_SERVER, CA_CERT); + return cfg; + } + + PropertyObjectPtr bothChannelsServerConfig(const InstancePtr& instance, int wsPort, int wssPort) + { + auto cfg = baseServerConfig(instance); + cfg.setPropertyValue(PROPERTY_ENABLE_WS_STREAMING_PORT_SERVER, True); + cfg.setPropertyValue(PROPERTY_ENABLE_WS_CONTROL_PORT_SERVER, False); + cfg.setPropertyValue(PROPERTY_WS_STREAMING_PORT_SERVER, wsPort); + cfg.setPropertyValue(PROPERTY_ENABLE_WSS_STREAMING_PORT_SERVER, True); + cfg.setPropertyValue(PROPERTY_WSS_STREAMING_PORT_SERVER, wssPort); + cfg.setPropertyValue(PROPERTY_ENABLE_MTLS_SERVER, False); + cfg.setPropertyValue(PROPERTY_WSS_CERT_FILE_PATH_SERVER, SERVER_CERT); + cfg.setPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_SERVER, SERVER_KEY); + return cfg; + } + + InstancePtr createClientInstance() + { + return Instance("."); + } + + PropertyObjectPtr secureDeviceConfig(const InstancePtr& instance, bool mtls, const std::string& caFile) + { + auto cfg = instance.getAvailableDeviceTypes().get(SECURE_DEVICE_TYPE_ID).createDefaultConfig(); + cfg.setPropertyValue(PROPERTY_ENABLE_MTLS_CLIENT, mtls); + cfg.setPropertyValue(PROPERTY_WSS_CA_CERT_FILE_PATH_CLIENT, String(caFile)); + if (mtls) + { + cfg.setPropertyValue(PROPERTY_WSS_CERT_FILE_PATH_CLIENT, CLIENT_CERT); + cfg.setPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_CLIENT, CLIENT_KEY); + } + return cfg; + } + + static SignalPtr findSignal(const DevicePtr& device, const std::string& name) + { + for (const auto& signal : device.getSignals(search::Recursive(search::Visible()))) + { + const auto descriptor = signal.getDescriptor(); + if (descriptor.assigned() && descriptor.getName() == name) + return signal; + } + return nullptr; + } + + // LT client mirrors remote signals asynchronously + // (an initial subscribe round-trip is needed to learn their metadata) + static SignalPtr getSignal(const DevicePtr& device, const std::string& name, std::chrono::seconds timeout = 10s) + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + do + { + if (auto signal = findSignal(device, name); signal.assigned()) + return signal; + std::this_thread::sleep_for(50ms); + } while (std::chrono::steady_clock::now() < deadline); + throw NotFoundException("signal '" + name + "' not found"); + } + + static std::vector readSamples(const StreamReaderPtr& reader, SizeT count, std::chrono::seconds timeout) + { + std::vector out; + std::vector chunk(count); + out.reserve(count); + const auto deadline = std::chrono::steady_clock::now() + timeout; + + while (out.size() < count && std::chrono::steady_clock::now() < deadline) + { + SizeT toRead = count - out.size(); + reader.read(chunk.data(), &toRead, 200); + if (toRead > 0) + out.insert(out.end(), chunk.begin(), chunk.begin() + toRead); + } + return out; + } + + void generatePackets(const InstancePtr& serverInstance, size_t packetCount) + { + for (const auto& device : serverInstance.getDevices()) + if (device.getName() == "MockPhysicalDevice") + device.setPropertyValue("GeneratePackets", static_cast(packetCount)); + } + + void runStreamingExchange(const InstancePtr& serverInstance, const DevicePtr& clientDevice) + { + auto serverSignal = getSignal(serverInstance, SIGNAL_NAME); + auto clientSignal = getSignal(clientDevice, SIGNAL_NAME); + + auto mirrored = clientSignal.asPtr(); + std::promise subscribePromise; + std::future subscribeFuture = subscribePromise.get_future(); + mirrored.getOnSubscribeComplete() += [&subscribePromise](MirroredSignalConfigPtr&, SubscriptionEventArgsPtr&) + { + try { subscribePromise.set_value(true); } + catch (const std::future_error&) { } + }; + + auto serverReader = StreamReader(serverSignal); + auto clientReader = StreamReader(clientSignal); + + ASSERT_EQ(subscribeFuture.wait_for(10s), std::future_status::ready) + << "client did not receive a subscribe acknowledgement"; + + constexpr size_t packetsToGenerate = 20; + constexpr SizeT samplesToRead = 10; + generatePackets(serverInstance, packetsToGenerate); + + auto serverSamples = readSamples(serverReader, samplesToRead, 15s); + auto clientSamples = readSamples(clientReader, samplesToRead, 15s); + + ASSERT_EQ(clientSamples.size(), samplesToRead) << "client did not receive the expected number of samples"; + ASSERT_EQ(serverSamples.size(), samplesToRead) << "server did not produce the expected number of samples"; + EXPECT_EQ(clientSamples, serverSamples) << "client samples do not match server samples"; + } +}; + +// Non-secure ws:// +TEST_F(LtStreamingTlsTest, WsBaselineStreaming) +{ + auto server = createServerInstanceWithDevice(); + server.addServer(SERVER_TYPE_ID, insecureServerConfig(server, 7610)); + + auto client = createClientInstance(); + auto device = client.addDevice("daq.lt://127.0.0.1:7610/"); + runStreamingExchange(server, device); +} + +// Secure (no mutual TLS) wss:// streaming +TEST_F(LtStreamingTlsTest, WssServerAuthStreaming) +{ + auto server = createServerInstanceWithDevice(); + server.addServer(SERVER_TYPE_ID, secureServerConfig(server, 7611, /*mtls*/ false)); + + auto client = createClientInstance(); + auto device = client.addDevice("daq.lts://127.0.0.1:7611/", secureDeviceConfig(client, /*mtls*/ false, CA_CERT)); + runStreamingExchange(server, device); +} + +// Secure mutual-TLS wss:// streaming +TEST_F(LtStreamingTlsTest, WssMutualTlsStreaming) +{ + auto server = createServerInstanceWithDevice(); + server.addServer(SERVER_TYPE_ID, secureServerConfig(server, 7612, /*mtls*/ true)); + + auto client = createClientInstance(); + auto device = client.addDevice("daq.lts://127.0.0.1:7612/", secureDeviceConfig(client, /*mtls*/ true, CA_CERT)); + runStreamingExchange(server, device); +} + +// A single server exposing both ws:// and wss:// listeners; the client connects over the secure one +TEST_F(LtStreamingTlsTest, BothChannelsSecureClient) +{ + auto server = createServerInstanceWithDevice(); + server.addServer(SERVER_TYPE_ID, bothChannelsServerConfig(server, 7615, 7616)); + + auto client = createClientInstance(); + auto device = client.addDevice("daq.lts://127.0.0.1:7616/", secureDeviceConfig(client, /*mtls*/ false, CA_CERT)); + runStreamingExchange(server, device); +} + +// A single server exposing both ws:// and wss:// listeners; the client connects over the plaintext one +TEST_F(LtStreamingTlsTest, BothChannelsInsecureClient) +{ + auto server = createServerInstanceWithDevice(); + server.addServer(SERVER_TYPE_ID, bothChannelsServerConfig(server, 7617, 7618)); + + auto client = createClientInstance(); + auto device = client.addDevice("daq.lt://127.0.0.1:7617/"); + runStreamingExchange(server, device); +} + +// A client that trusts the wrong CA must fail to connect to the secure server +TEST_F(LtStreamingTlsTest, WssUntrustedCaRejected) +{ + auto server = createServerInstanceWithDevice(); + server.addServer(SERVER_TYPE_ID, secureServerConfig(server, 7613, /*mtls*/ false)); + + auto client = createClientInstance(); + + ASSERT_THROW(client.addDevice("daq.lts://127.0.0.1:7613/", secureDeviceConfig(client, /*mtls*/ false, OTHER_CA_CERT)), + AuthenticationFailedException); +} + +// A secure connection with no CA configured must be rejected before any connection is attempted +TEST_F(LtStreamingTlsTest, WssMissingCaRejected) +{ + auto server = createServerInstanceWithDevice(); + server.addServer(SERVER_TYPE_ID, secureServerConfig(server, 7614, /*mtls*/ false)); + + auto client = createClientInstance(); + ASSERT_THROW(client.addDevice("daq.lts://127.0.0.1:7614/", secureDeviceConfig(client, /*mtls*/ false, "")), + InvalidParameterException); +} + +TEST_F(LtStreamingTlsTest, WssUnreachableServerReportedAsNotFound) +{ + auto client = createClientInstance(); + ASSERT_THROW(client.addDevice("daq.lts://127.0.0.1:7623/", secureDeviceConfig(client, /*mtls*/ false, CA_CERT)), + NotFoundException); +} + +TEST_F(LtStreamingTlsTest, ServerRejectsMissingCertificateFile) +{ + auto instance = createServerInstanceWithDevice(); + auto config = secureServerConfig(instance, 7619, /*mtls*/ false); + config.setPropertyValue(PROPERTY_WSS_CERT_FILE_PATH_SERVER, "secrets/does-not-exist.crt"); + + ASSERT_THROW_MSG(instance.addServer(SERVER_TYPE_ID, config), + InvalidParameterException, + "Cannot load the TLS secrets"); +} + +TEST_F(LtStreamingTlsTest, ServerRejectsMismatchedKey) +{ + auto instance = createServerInstanceWithDevice(); + auto config = secureServerConfig(instance, 7620, /*mtls*/ false); + config.setPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_SERVER, CLIENT_KEY); + + ASSERT_THROW_MSG(instance.addServer(SERVER_TYPE_ID, config), + InvalidParameterException, + "Cannot load the TLS secrets"); +} + +TEST_F(LtStreamingTlsTest, ClientRejectsMissingKeyFile) +{ + auto client = createClientInstance(); + auto config = secureDeviceConfig(client, /*mtls*/ true, CA_CERT); + config.setPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_CLIENT, "secrets/does-not-exist.key"); + + ASSERT_THROW(client.addDevice("daq.lts://127.0.0.1:7621/", config), InvalidParameterException); +} + +TEST_F(LtStreamingTlsTest, ClientRejectsMismatchedKey) +{ + auto client = createClientInstance(); + auto config = secureDeviceConfig(client, /*mtls*/ true, CA_CERT); + config.setPropertyValue(PROPERTY_WSS_KEY_FILE_PATH_CLIENT, SERVER_KEY); + + ASSERT_THROW(client.addDevice("daq.lts://127.0.0.1:7622/", config), InvalidParameterException); +}