diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml index 3b5b400..ffd89d9 100644 --- a/.github/workflows/host-tests.yml +++ b/.github/workflows/host-tests.yml @@ -4,7 +4,7 @@ on: push: branches: - main - - feature/command-socket-integration + - feature/observable-callback-coverage pull_request: jobs: @@ -14,13 +14,20 @@ jobs: - name: Checkout Sockets uses: actions/checkout@v4 - - name: Checkout ESPressio Command 0.2.0 + - name: Checkout ESPressio Command 0.3.0 uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Command - ref: 0.2.0 + ref: 0.3.0 path: deps/ESPressio-Command + - name: Checkout ESPressio Security 0.2.0 + uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Security + ref: 0.2.0 + path: deps/ESPressio-Security + - name: Checkout ESPressio Timing 2.2.2 uses: actions/checkout@v4 with: @@ -46,6 +53,7 @@ jobs: run: >- cmake -S tests -B build -DESPRESSIO_COMMAND_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Command/src" + -DESPRESSIO_SECURITY_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Security/src" -DESPRESSIO_TIMING_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Timing/src" -DESPRESSIO_UNITS_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Units/src" -DESPRESSIO_OBSERVABLE_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Observable/src" diff --git a/.github/workflows/security-integration-tests.yml b/.github/workflows/security-integration-tests.yml index d7412bc..2b653e8 100644 --- a/.github/workflows/security-integration-tests.yml +++ b/.github/workflows/security-integration-tests.yml @@ -2,7 +2,7 @@ name: Security Integration on: push: - branches: [feature/security-integration, main] + branches: [feature/security-integration, feature/observable-callback-coverage, main] pull_request: branches: [main] @@ -14,12 +14,12 @@ jobs: - uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Command - ref: 0.2.0 + ref: 0.3.0 path: deps/ESPressio-Command - uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Security - ref: 0.1.0 + ref: 0.2.0 path: deps/ESPressio-Security - uses: actions/checkout@v4 with: @@ -56,13 +56,18 @@ jobs: - uses: actions/checkout@v4 with: repository: Flowduino/ESPressio-Security - ref: 0.1.0 + ref: 0.2.0 path: deps/ESPressio-Security + - uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Observable + ref: 3.0.1 + path: deps/ESPressio-Observable - name: Install PlatformIO run: pip install platformio - name: Compile SecureTCPClient run: >- pio ci examples/SecureTCPClient/SecureTCPClient.ino - --board esp32dev --lib . --lib deps/ESPressio-Security - --project-option="build_flags=-std=gnu++17" + --board esp32dev --lib . --lib deps/ESPressio-Security --lib deps/ESPressio-Observable + --project-option="build_flags=-std=gnu++17 -frtti" --project-option="build_unflags=-std=gnu++11 -fno-rtti" diff --git a/CHANGELOG.md b/CHANGELOG.md index 031adc6..293a1c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.5.0 — 2026-08-20 + +### Added +- Added `ISocketWorkerObserver` and observable socket-worker lifecycle notifications for start, start failure, and stop transitions. +- Added `ISocketSecuritySessionObserver` and observable secure-session fault/reset notifications. +- Added ESPressio Observable >= 3.0.1 < 4.0.0 as the common lifecycle-observer dependency. +- Added optional ESPressio Event bridge support through ESPressio Event 5.8.0. + +### Changed +- Updated the validated optional ESPressio Security baseline to Security >= 0.2.0 < 1.0.0. +- Security session send/unprotect/frame-limit failures now publish lifecycle observations while preserving existing result and callback behavior. +- Bumped package/component/public version metadata to 0.5.0. +- Event, Timing, Command and Security integrations remain opt-in. + ## 0.4.0 — 2026-08-20 ### Added diff --git a/README.md b/README.md index c0401d5..1e99f9d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,35 @@ Socket-based ESPressio transports, Command adapters, transport-security sessions The latest Stable Version is **0.4.0**. +## Current Development Version — 0.5.0 + +The `feature/observable-callback-coverage` branch targets **0.5.0** and adds native Observable lifecycle coverage while preserving the existing socket/data callback model. + +For this development branch, the dependency model is: + +```text +Required + ESPressio Observable >= 3.0.1 < 4.0.0 + +Optional Command integration + ESPressio Command >= 0.3.0 < 1.0.0 + +Optional Transport Security + ESPressio Security >= 0.2.0 < 1.0.0 + +Optional Event integration + ESPressio Event >= 5.8.0 < 6.0.0 + +Optional Timing synchronization + ESPressio Timing >= 2.2.2 < 3.0.0 +``` + +Observable coverage is owned by Sockets itself. `SocketWorker` exposes start/start-failure/stop lifecycle observation, and `SocketSecuritySession` exposes secure-session fault/reset observation. Existing receive, write, Command, Event Transport, and security-processing callbacks remain authoritative for their original responsibilities. + +ESPressio Event remains **opt-in**. Event 5.8 provides `SocketWorkerEventBridge` and `SocketSecuritySessionEventBridge`; Sockets does not depend upward on Event. ESPressio Serial 0.5 can consume the same observer contracts directly for diagnostics without requiring Event. + +The stable-release documentation below remains intact so existing 0.4.0 users retain accurate historical guidance. + ## Compatibility ESPressio Sockets `0.4.0` targets ESP32/Arduino-ESP32 and C++17. Individual facilities may depend on Arduino networking classes, WebSockets, MQTT, or optional ESPressio libraries according to the adapter selected. @@ -20,9 +49,9 @@ Apache License 2.0. See [LICENSE](LICENSE). ## ESPressio Library Dependencies -Core ESPressio Sockets has no mandatory ESPressio dependency. +Core ESPressio Sockets has no mandatory ESPressio dependency in the stable 0.4.0 release. The 0.5.0 development branch adds the required Observable dependency documented above. -Optional integrations: +Optional integrations for stable 0.4.0: ```text Event transports @@ -74,6 +103,8 @@ lib_deps = Add Event, Command, or Timing only when selecting those integrations. +For the 0.5.0 development branch, also include ESPressio Observable 3.0.1 or newer within the 3.x line, and use the Command/Security floors listed in the development-version section above. + ## Header Structure The normal umbrella is: @@ -174,7 +205,7 @@ The incoming datagram is passed to `Receive()` and is delivered upward only afte ### Security Guarantees -The adapters inherit Security 0.1.x semantics: +The stable 0.4.0 adapters inherit Security 0.1.x semantics: ```text AEAD encryption/authentication @@ -207,7 +238,9 @@ The same architecture applies to Event or future application protocols. ## Failure Observation -Both Security adapters expose a failure callback carrying `SecurityResult`. This provides error classification for authentication, replay, key, algorithm, protocol, and frame-limit failures without exposing secret key material. +Stable 0.4.0 Security adapters expose a failure callback carrying `SecurityResult`. This provides error classification for authentication, replay, key, algorithm, protocol, and frame-limit failures without exposing secret key material. + +The 0.5.0 development branch additionally exposes the corresponding `ISocketSecuritySessionObserver` lifecycle contract and the general `ISocketWorkerObserver` lifecycle contract. These observer notifications complement rather than replace existing operational callbacks. ## Examples @@ -230,9 +263,7 @@ SocketSecurity ClockSynchronizationProtocol ``` -`SocketSecurity` covers fragmented stream input, coalesced stream frames, declared-size limits, stream reset behavior, datagram protection and replay rejection. - -Permanent CI checks out released ESPressio Security 0.1.0 and compiles the real ESP32 secure TCP example in addition to host tests. +`SocketSecurity` covers fragmented stream input, coalesced stream frames, declared-size limits, stream reset behavior, datagram protection and replay rejection. The 0.5.0 development branch extends host validation to the new observer lifecycle surface and tests against the refreshed Command/Security/Observable dependency generation. ## Compatibility @@ -245,6 +276,8 @@ Sockets 0.4.0 is a backward-compatible minor release: - Security integration is opt-in; - the normal umbrella remains independent of Security. +The 0.5.0 development branch is also designed as a backward-compatible minor extension. Observable becomes a core dependency because core worker lifecycle is now observable; Event itself remains optional. + ## Contributing Issues and contributions are welcome through GitHub. New socket mechanisms should keep application semantics and cryptography outside their concrete I/O responsibility wherever possible. diff --git a/component.mk b/component.mk index d4fe052..51daf11 100644 --- a/component.mk +++ b/component.mk @@ -4,6 +4,6 @@ CXXFLAGS += -std=gnu++17 CPPFLAGS += \ -DESPRESSIO_SOCKETS \ -DESPRESSIO_SOCKETS_VERSION_MAJOR=0 \ - -DESPRESSIO_SOCKETS_VERSION_MINOR=4 \ + -DESPRESSIO_SOCKETS_VERSION_MINOR=5 \ -DESPRESSIO_SOCKETS_VERSION_PATCH=0 \ - -DESPRESSIO_SOCKETS_VERSION_STRING=\"0.4.0\" + -DESPRESSIO_SOCKETS_VERSION_STRING=\"0.5.0\" diff --git a/library.json b/library.json index 427a219..9faa89c 100644 --- a/library.json +++ b/library.json @@ -1,7 +1,7 @@ { "name": "ESPressio-Sockets", "description": "Socket-based ESPressio transports, Command invocation adapters, Security sessions, and Timing synchronization providers for ESP32.", - "keywords": "esp32,sockets,udp,tcp,tls,websocket,mqtt,event,command,security,encryption,transport,network,timing,clock,synchronization,espressio", + "keywords": "esp32,sockets,udp,tcp,tls,websocket,mqtt,event,command,security,encryption,transport,network,timing,clock,synchronization,observable,espressio", "authors": { "name": "Flowduino", "maintainer": true, @@ -16,11 +16,16 @@ "type": "git", "url": "https://github.com/Flowduino/ESPressio-Sockets.git" }, - "version": "0.4.0", + "version": "0.5.0", "license": "Apache-2.0", "frameworks": "arduino", "platforms": "espressif32", "dependencies": [ + { + "name": "Flowduino ESPressio-Observable", + "version": ">=3.0.1 <4.0.0", + "url": "https://github.com/Flowduino/ESPressio-Observable.git" + }, { "owner": "links2004", "name": "WebSockets", diff --git a/library.properties b/library.properties index 3ab0034..bd4d6f7 100644 --- a/library.properties +++ b/library.properties @@ -1,11 +1,11 @@ name=ESPressio-Sockets -version=0.4.0 +version=0.5.0 author=Flowduino maintainer=Flowduino sentence=Socket-based Event Transport, Command invocation, Security sessions, and System Clock synchronization for the ESPressio ecosystem. -paragraph=Provides UDP, TCP, TLS, WebSocket and MQTT Event transports, opt-in TCP ESPressio Command invocation, opt-in ESPressio Security stream/datagram protection, and opt-in Timing synchronization. +paragraph=Provides UDP, TCP, TLS, WebSocket and MQTT Event transports, opt-in TCP ESPressio Command invocation, opt-in ESPressio Security stream/datagram protection, observable worker/session lifecycle notifications, and opt-in Timing synchronization. category=Communication url=https://github.com/Flowduino/ESPressio-Sockets architectures=esp32 includes=ESPressio_Sockets.hpp -depends=WebSockets,PubSubClient +depends=Flowduino ESPressio-Observable (>=3.0.1),WebSockets,PubSubClient diff --git a/src/ESPressio_ISocketSecuritySessionObserver.hpp b/src/ESPressio_ISocketSecuritySessionObserver.hpp new file mode 100644 index 0000000..2a074ee --- /dev/null +++ b/src/ESPressio_ISocketSecuritySessionObserver.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace ESPressio::Sockets { + +class ISocketSecuritySessionObserver : + public virtual Observable::IObserver { +public: + virtual ~ISocketSecuritySessionObserver() = default; + + virtual void OnSocketSecuritySessionFaulted( + const Security::SecurityResult& + ) {} + + virtual void OnSocketSecuritySessionReset() {} +}; + +} // namespace ESPressio::Sockets diff --git a/src/ESPressio_ISocketWorkerObserver.hpp b/src/ESPressio_ISocketWorkerObserver.hpp new file mode 100644 index 0000000..963db23 --- /dev/null +++ b/src/ESPressio_ISocketWorkerObserver.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +namespace ESPressio::Sockets { + +class ISocketWorkerObserver : + public virtual Observable::IObserver { +public: + virtual ~ISocketWorkerObserver() = default; + + virtual void OnSocketWorkerStarted( + const char* + ) {} + + virtual void OnSocketWorkerStartFailed( + const char* + ) {} + + virtual void OnSocketWorkerStopped() {} +}; + +} // namespace ESPressio::Sockets diff --git a/src/ESPressio_SocketSecuritySession.hpp b/src/ESPressio_SocketSecuritySession.hpp index aae97b4..168d41a 100644 --- a/src/ESPressio_SocketSecuritySession.hpp +++ b/src/ESPressio_SocketSecuritySession.hpp @@ -3,11 +3,15 @@ #include #include #include +#include #include #include +#include #include +#include "ESPressio_ISocketSecuritySessionObserver.hpp" + namespace ESPressio::Sockets { struct SocketSecuritySessionConfig { @@ -20,18 +24,88 @@ class SocketSecuritySession final { using ReceiveCallback = std::function; using FailureCallback = std::function; +private: + class SessionObservable final : public Observable::ThreadSafeObservable { + private: + template + void Notify(Callback&& callback) { + ExecuteNotification([&](NotificationContext& notification) { + notification.WithObservers([&](ISocketSecuritySessionObserver* observer) { + try { callback(observer); } catch (...) {} + }); + }); + } + public: + void Faulted(const Security::SecurityResult& result) { + Notify([&](ISocketSecuritySessionObserver* observer){ observer->OnSocketSecuritySessionFaulted(result); }); + } + void Reset() { + Notify([](ISocketSecuritySessionObserver* observer){ observer->OnSocketSecuritySessionReset(); }); + } + }; + + Security::TransportSecurity& _security; + WriteCallback _writer; + SocketSecuritySessionConfig _config; + ReceiveCallback _receive; + FailureCallback _failure; + std::vector _buffer; + bool _discarding = false; + std::shared_ptr _observable = std::make_shared(); + + void PublishFailure(const Security::SecurityResult& failure) { + if (_failure) _failure(failure); + _observable->Faulted(failure); + } + + void ProcessEnvelope(uint8_t protocol, const uint8_t* envelope, std::size_t size) { + Security::UnprotectedPayload opened; + auto result = _security.Unprotect(protocol, envelope, size, opened); + if (!result.Success) { + PublishFailure(result); + return; + } + if (_receive) _receive(opened); + } + + static void Append32(std::vector& out, uint32_t value) { + for (int i=0;i<4;++i) out.push_back(static_cast(value >> (i*8))); + } + static uint32_t Read32(const uint8_t* p) { + return static_cast(p[0]) | (static_cast(p[1])<<8) | + (static_cast(p[2])<<16) | (static_cast(p[3])<<24); + } + +public: SocketSecuritySession(Security::TransportSecurity& security, WriteCallback writer, SocketSecuritySessionConfig config = {}) : _security(security), _writer(std::move(writer)), _config(config) {} void SetReceiveCallback(ReceiveCallback callback) { _receive = std::move(callback); } void SetFailureCallback(FailureCallback callback) { _failure = std::move(callback); } + Observable::ObserverHandlePtr RegisterObserver(ISocketSecuritySessionObserver* observer) { + return _observable->RegisterObserver(observer); + } + + void UnregisterObserver(ISocketSecuritySessionObserver* observer) { + _observable->UnregisterObserver(observer); + } + bool Send(uint8_t protocol, const void* payload, std::size_t payloadLength, Security::SecurityResult* resultOut = nullptr) { if (!_writer || (payload == nullptr && payloadLength != 0)) return false; std::vector protectedBytes; auto result = _security.Protect(protocol, static_cast(payload), payloadLength, protectedBytes); if (resultOut) *resultOut = result; - if (!result.Success || protectedBytes.empty() || protectedBytes.size() > _config.MaximumProtectedFrameBytes || protectedBytes.size() > 0xFFFFFFFFu) return false; + if (!result.Success) { + PublishFailure(result); + return false; + } + if (protectedBytes.empty() || protectedBytes.size() > _config.MaximumProtectedFrameBytes || protectedBytes.size() > 0xFFFFFFFFu) { + auto failure = Security::SecurityResult::Fail(Security::SecurityError::BufferLimitExceeded, "Protected secure socket frame is empty or exceeds configured limit"); + if (resultOut) *resultOut = failure; + PublishFailure(failure); + return false; + } std::vector frame; frame.reserve(5 + protectedBytes.size()); Append32(frame, static_cast(protectedBytes.size())); @@ -47,9 +121,10 @@ class SocketSecuritySession final { if (_buffer.size() < 5) return true; const uint32_t length = Read32(_buffer.data()); if (length == 0 || length > _config.MaximumProtectedFrameBytes) { - _buffer.clear(); _discarding = true; + _buffer.clear(); + _discarding = true; auto failure = Security::SecurityResult::Fail(Security::SecurityError::BufferLimitExceeded, "Secure socket frame length is invalid or exceeds configured limit"); - if (_failure) _failure(failure); + PublishFailure(failure); return false; } if (_buffer.size() < 5 + static_cast(length)) return true; @@ -59,32 +134,13 @@ class SocketSecuritySession final { } } - void Reset() { _buffer.clear(); _discarding = false; } - std::size_t BufferedBytes() const noexcept { return _buffer.size(); } - -private: - Security::TransportSecurity& _security; - WriteCallback _writer; - SocketSecuritySessionConfig _config; - ReceiveCallback _receive; - FailureCallback _failure; - std::vector _buffer; - bool _discarding = false; - - void ProcessEnvelope(uint8_t protocol, const uint8_t* envelope, std::size_t size) { - Security::UnprotectedPayload opened; - auto result = _security.Unprotect(protocol, envelope, size, opened); - if (!result.Success) { if (_failure) _failure(result); return; } - if (_receive) _receive(opened); + void Reset() { + _buffer.clear(); + _discarding = false; + _observable->Reset(); } - static void Append32(std::vector& out, uint32_t value) { - for (int i=0;i<4;++i) out.push_back(static_cast(value >> (i*8))); - } - static uint32_t Read32(const uint8_t* p) { - return static_cast(p[0]) | (static_cast(p[1])<<8) | - (static_cast(p[2])<<16) | (static_cast(p[3])<<24); - } + std::size_t BufferedBytes() const noexcept { return _buffer.size(); } }; -} +} // namespace ESPressio::Sockets diff --git a/src/ESPressio_SocketWorker.hpp b/src/ESPressio_SocketWorker.hpp index 074834c..d5ea5f3 100644 --- a/src/ESPressio_SocketWorker.hpp +++ b/src/ESPressio_SocketWorker.hpp @@ -1,120 +1,110 @@ #pragma once #include +#include +#include + +#include "ESPressio_ISocketWorkerObserver.hpp" #include "ESPressio_SocketTypes.hpp" namespace ESPressio::Sockets { class SocketWorker { private: + class WorkerObservable final : public Observable::ThreadSafeObservable { + private: + template + void Notify(Callback&& callback) { + ExecuteNotification([&](NotificationContext& notification) { + notification.WithObservers([&](ISocketWorkerObserver* observer) { + try { callback(observer); } catch (...) {} + }); + }); + } + public: + void Started(const char* name) { Notify([&](ISocketWorkerObserver* observer){ observer->OnSocketWorkerStarted(name); }); } + void StartFailed(const char* name) { Notify([&](ISocketWorkerObserver* observer){ observer->OnSocketWorkerStartFailed(name); }); } + void Stopped() { Notify([](ISocketWorkerObserver* observer){ observer->OnSocketWorkerStopped(); }); } + }; + TaskHandle_t _taskHandle = nullptr; std::atomic _running{false}; SocketWorkerConfig _config; + std::shared_ptr _observable = std::make_shared(); - static void TaskEntry( - void* parameter - ) { - auto* worker = - static_cast( - parameter - ); - - if (worker != nullptr) { - worker->Run(); - } - + static void TaskEntry(void* parameter) { + auto* worker = static_cast(parameter); + if (worker != nullptr) worker->Run(); vTaskDelete(nullptr); } void Run() { while (_running.load()) { OnWorkerIteration(); - - if ( - _config.IdleDelayMilliseconds > - 0 - ) { - vTaskDelay( - pdMS_TO_TICKS( - _config. - IdleDelayMilliseconds - ) - ); + if (_config.IdleDelayMilliseconds > 0) { + vTaskDelay(pdMS_TO_TICKS(_config.IdleDelayMilliseconds)); } else { taskYIELD(); } } - _taskHandle = nullptr; } protected: virtual void OnWorkerIteration() = 0; - bool StartWorker( - const char* name, - const SocketWorkerConfig& config - ) { - if (_running.load()) { - return true; - } + bool StartWorker(const char* name, const SocketWorkerConfig& config) { + if (_running.load()) return true; _config = config; _running.store(true); - const BaseType_t result = - xTaskCreatePinnedToCore( - TaskEntry, - name, - config.StackSize, - this, - config.Priority, - &_taskHandle, - config.Core - ); + const BaseType_t result = xTaskCreatePinnedToCore( + TaskEntry, + name, + config.StackSize, + this, + config.Priority, + &_taskHandle, + config.Core + ); if (result != pdPASS) { _running.store(false); _taskHandle = nullptr; + _observable->StartFailed(name); return false; } + _observable->Started(name); return true; } void StopWorker() { - _running.store(false); - - if ( - _taskHandle == nullptr || - xTaskGetCurrentTaskHandle() == - _taskHandle - ) { - return; - } + const bool wasRunning = _running.exchange(false); - /* - * Worker loops are deliberately non-blocking or use short timeouts. - * Wait for natural exit before derived classes destroy their socket - * resources. - */ - while (_taskHandle != nullptr) { - vTaskDelay( - pdMS_TO_TICKS(1) - ); + if (_taskHandle != nullptr && xTaskGetCurrentTaskHandle() != _taskHandle) { + while (_taskHandle != nullptr) { + vTaskDelay(pdMS_TO_TICKS(1)); + } } + + if (wasRunning) _observable->Stopped(); } public: - virtual ~SocketWorker() { - StopWorker(); + virtual ~SocketWorker() { StopWorker(); } + + Observable::ObserverHandlePtr RegisterObserver(ISocketWorkerObserver* observer) { + return _observable->RegisterObserver(observer); } - bool GetWorkerIsRunning() const - noexcept { - return _running.load(); + void UnregisterObserver(ISocketWorkerObserver* observer) { + _observable->UnregisterObserver(observer); } + + bool GetWorkerIsRunning() const noexcept { return _running.load(); } }; -} +} // namespace ESPressio::Sockets diff --git a/src/ESPressio_Sockets.hpp b/src/ESPressio_Sockets.hpp index 2fb81ce..bb18db5 100644 --- a/src/ESPressio_Sockets.hpp +++ b/src/ESPressio_Sockets.hpp @@ -4,13 +4,13 @@ #define ESPRESSIO_SOCKETS_VERSION_MAJOR 0 #endif #ifndef ESPRESSIO_SOCKETS_VERSION_MINOR -#define ESPRESSIO_SOCKETS_VERSION_MINOR 4 +#define ESPRESSIO_SOCKETS_VERSION_MINOR 5 #endif #ifndef ESPRESSIO_SOCKETS_VERSION_PATCH #define ESPRESSIO_SOCKETS_VERSION_PATCH 0 #endif #ifndef ESPRESSIO_SOCKETS_VERSION_STRING -#define ESPRESSIO_SOCKETS_VERSION_STRING "0.4.0" +#define ESPRESSIO_SOCKETS_VERSION_STRING "0.5.0" #endif #include "ESPressio_SocketTypes.hpp" @@ -20,6 +20,12 @@ * Dependency-bearing integrations are deliberately NOT batch-included here. * Include only the facilities required by the project. * + * Observable lifecycle: + * ESPressio_SocketWorker.hpp + * ESPressio_ISocketWorkerObserver.hpp + * ESPressio_SocketSecuritySession.hpp + * ESPressio_ISocketSecuritySessionObserver.hpp + * * Event transports: * ESPressio_UDPEventTransport.hpp * ESPressio_TCPClientEventTransport.hpp @@ -38,10 +44,11 @@ * ESPressio_SocketCommandSession.hpp * ESPressio_TCPCommandServer.hpp * - * Security: + * Security (validated against ESPressio Security >=0.2.0 <1.0.0): * ESPressio_SocketSecuritySession.hpp * ESPressio_SocketSecurityDatagram.hpp * - * This keeps Event/Serializable, Timing, Command, and Security dependencies - * opt-in at the consuming-code level. + * Event, Timing, Command, and Security integrations remain opt-in at the + * consuming-code level. Observable is the common lifecycle-notification + * dependency used by socket workers and secure sessions. */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b925192..7a850e0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,10 +13,14 @@ if(NOT ESPRESSIO_COMMAND_INCLUDE_DIR) message(FATAL_ERROR "ESPRESSIO_COMMAND_INCLUDE_DIR is required for SocketCommand tests") endif() +if(NOT ESPRESSIO_OBSERVABLE_INCLUDE_DIR) + message(FATAL_ERROR "ESPRESSIO_OBSERVABLE_INCLUDE_DIR is required for observable Sockets tests") +endif() + add_executable(test_socket_command test_socket_command.cpp) target_compile_features(test_socket_command PRIVATE cxx_std_17) target_compile_options(test_socket_command PRIVATE -Wall -Wextra -Wpedantic -Werror) -target_include_directories(test_socket_command PRIVATE ../src ${ESPRESSIO_COMMAND_INCLUDE_DIR}) +target_include_directories(test_socket_command PRIVATE ../src ${ESPRESSIO_COMMAND_INCLUDE_DIR} ${ESPRESSIO_OBSERVABLE_INCLUDE_DIR}) add_test(NAME SocketCommand COMMAND test_socket_command) if(NOT ESPRESSIO_SECURITY_INCLUDE_DIR) @@ -26,9 +30,15 @@ endif() add_executable(test_socket_security test_socket_security.cpp) target_compile_features(test_socket_security PRIVATE cxx_std_17) target_compile_options(test_socket_security PRIVATE -Wall -Wextra -Wpedantic -Werror) -target_include_directories(test_socket_security PRIVATE ../src ${ESPRESSIO_SECURITY_INCLUDE_DIR}) +target_include_directories(test_socket_security PRIVATE ../src ${ESPRESSIO_SECURITY_INCLUDE_DIR} ${ESPRESSIO_OBSERVABLE_INCLUDE_DIR}) add_test(NAME SocketSecurity COMMAND test_socket_security) +add_executable(test_socket_observable test_socket_observable.cpp) +target_compile_features(test_socket_observable PRIVATE cxx_std_17) +target_compile_options(test_socket_observable PRIVATE -Wall -Wextra -Wpedantic -Werror) +target_include_directories(test_socket_observable PRIVATE ../src ${ESPRESSIO_SECURITY_INCLUDE_DIR} ${ESPRESSIO_OBSERVABLE_INCLUDE_DIR}) +add_test(NAME SocketObservable COMMAND test_socket_observable) + if(ESPRESSIO_TIMING_INCLUDE_DIR AND ESPRESSIO_UNITS_INCLUDE_DIR AND ESPRESSIO_OBSERVABLE_INCLUDE_DIR) add_executable(test_clock_sync_protocol test_clock_sync_protocol.cpp) target_compile_features(test_clock_sync_protocol PRIVATE cxx_std_17) diff --git a/tests/test_socket_observable.cpp b/tests/test_socket_observable.cpp new file mode 100644 index 0000000..d815ecd --- /dev/null +++ b/tests/test_socket_observable.cpp @@ -0,0 +1,51 @@ +#include +#include +#include + +#include + +using namespace ESPressio; + +class SessionObserver final : public Sockets::ISocketSecuritySessionObserver { +public: + int Faulted = 0; + int Reset = 0; + + void OnSocketSecuritySessionFaulted(const Security::SecurityResult&) override { + ++Faulted; + } + + void OnSocketSecuritySessionReset() override { + ++Reset; + } +}; + +int main() { + Security::AeadCipherRegistry ciphers; + Security::StaticKeyProvider keys; + Security::StandardRandomSource random; + Security::TransportSecurityConfig config; + config.Policy = Security::TransportSecurityPolicy::Disabled; + Security::TransportSecurity security(ciphers, keys, random, config); + + Sockets::SocketSecuritySession session( + security, + [](const uint8_t*, std::size_t) { return true; } + ); + + SessionObserver observer; + auto handle = session.RegisterObserver(&observer); + assert(handle); + + const uint8_t malformedLength[5] = {0, 0, 0, 0, 1}; + assert(!session.Feed(malformedLength, sizeof(malformedLength))); + assert(observer.Faulted == 1); + + session.Reset(); + assert(observer.Reset == 1); + + handle.reset(); + session.Reset(); + assert(observer.Reset == 1); + return 0; +}