From 6f4705cd977932f0bc5f8ce126de0ffec9113dae Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:48:39 +0000 Subject: [PATCH 01/12] [WebSocket/WebSocketLink] : Enable unsollicited 'Pong' and rename enum item - Rename 'WEBSERVER' to 'WEBSERVICE' as both client and server utilize state information - Unsollicited 'Pong' may act as a heart beat --- Source/websocket/WebSocketLink.h | 42 ++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/Source/websocket/WebSocketLink.h b/Source/websocket/WebSocketLink.h index c1f3f14ad..3125b3ffe 100644 --- a/Source/websocket/WebSocketLink.h +++ b/Source/websocket/WebSocketLink.h @@ -35,6 +35,10 @@ namespace Web { CLOSE = 0x08, PING = 0x09, PONG = 0x0A, + // Reserved ranges + // 0x3-0x7 + // 0xB-0xF + // Following are outside reseved 4-bit ranges VIOLATION = 0x10, // e.g. a control package without a FIN flag TOO_BIG = 0x20, // Protocol max support for 2^16 message per chunk INCONSISTENT = 0x30 // e.g. Protocol defined as Text, but received a binary. @@ -182,7 +186,7 @@ namespace Web { class WebSocketLinkType { public: enum EnumlinkState : uint8_t { - WEBSERVER = 0x01, + WEBSERVICE = 0x01, UPGRADING = 0x02, WEBSOCKET = 0x04, SUSPENDED = 0x08, @@ -373,7 +377,7 @@ PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) , _handler(binary, masking) , _parent(parent) , _adminLock() - , _state(WEBSERVER) + , _state(WEBSERVICE) , _serializerImpl(*this, queueSize) , _deserialiserImpl(*this, queueSize) , _path() @@ -390,7 +394,7 @@ PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) , _handler(binary, masking) , _parent(parent) , _adminLock() - , _state(WEBSERVER) + , _state(WEBSERVICE) , _serializerImpl(*this, queueSize) , _deserialiserImpl(*this, allocator) , _path() @@ -419,7 +423,7 @@ POP_WARNING() } bool IsWebServer() const { - return ((State() & WEBSERVER) != 0); + return ((State() & WEBSERVICE) != 0); } bool IsUpgrading() const { @@ -477,6 +481,18 @@ POP_WARNING() ACTUALLINK::Trigger(); } + void Pong() + { + _pingFireTime = Core::Time::Now().Ticks(); + + _adminLock.Lock(); + + _handler.Pong(); + + _adminLock.Unlock(); + + ACTUALLINK::Trigger(); + } bool Masking() const { return (_handler.Masking()); @@ -775,7 +791,7 @@ POP_WARNING() // Multiple message might be coming in, protect the state before we make assumptions on it value. _adminLock.Lock(); - if ((_state & WEBSERVER) == 0) { + if ((_state & WEBSERVICE) == 0) { _webSocketMessage->ErrorCode = Web::STATUS_INTERNAL_SERVER_ERROR; _webSocketMessage->Message = _T("State of the link can not be upgraded."); } else { @@ -794,7 +810,7 @@ POP_WARNING() _parent.StateChange(); if (_webSocketMessage->ErrorCode != Web::STATUS_SWITCH_PROTOCOL) { - _state = (_state & 0xF0) | WEBSERVER; + _state = (_state & 0xF0) | WEBSERVICE; _path.clear(); _query.clear(); _protocol.Clear(); @@ -845,7 +861,7 @@ POP_WARNING() _adminLock.Lock(); - if ((_state & WEBSERVER) != 0) { + if ((_state & WEBSERVICE) != 0) { result = true; _state = (_state & 0xF0) | UPGRADING; _origin = (origin.empty() ? ACTUALLINK::LocalId() : origin); @@ -1076,6 +1092,10 @@ POP_WARNING() { _channel.Ping(); } + void Pong() + { + _channel.Pong(); + } void Trigger() { _channel.Trigger(); @@ -1272,6 +1292,10 @@ POP_WARNING() { _channel.Ping(); } + void Pong() + { + _channel.Pong(); + } virtual bool IsIdle() const = 0; virtual void StateChange() = 0; @@ -1430,6 +1454,10 @@ POP_WARNING() { _channel.Ping(); } + void Pong() + { + _channel.Pong(); + } virtual bool IsIdle() const = 0; virtual void StateChange() = 0; From aa8c25dac888767515c326a0616f5b235d0ce5b4 Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:24:54 +0000 Subject: [PATCH 02/12] [WebSocket/WebSocketLink] : METROL-1087 --- Source/websocket/WebSocketLink.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/websocket/WebSocketLink.h b/Source/websocket/WebSocketLink.h index 3125b3ffe..b5ddd7fba 100644 --- a/Source/websocket/WebSocketLink.h +++ b/Source/websocket/WebSocketLink.h @@ -193,6 +193,8 @@ namespace Web { ACTIVITY = 0x10 }; + DEPRECATED constexpr static EnumlinkState WEBSERVER { EnumlinkState::WEBSERVICE }; + typedef WebSocketLinkType ParentClass; private: @@ -377,7 +379,7 @@ PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) , _handler(binary, masking) , _parent(parent) , _adminLock() - , _state(WEBSERVICE) + , _state(WEBSERVER) , _serializerImpl(*this, queueSize) , _deserialiserImpl(*this, queueSize) , _path() From e923fcfdc67d04b8780f4b537ac77d36ceac55fe Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 7 Nov 2024 08:18:58 +0000 Subject: [PATCH 03/12] [WebSocket/WebSocketLink] : rename forgotten 'WEBSERVER' to 'WEBSERVICE' --- Source/websocket/WebSocketLink.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/websocket/WebSocketLink.h b/Source/websocket/WebSocketLink.h index b5ddd7fba..598f19bbc 100644 --- a/Source/websocket/WebSocketLink.h +++ b/Source/websocket/WebSocketLink.h @@ -379,7 +379,7 @@ PUSH_WARNING(DISABLE_WARNING_THIS_IN_MEMBER_INITIALIZER_LIST) , _handler(binary, masking) , _parent(parent) , _adminLock() - , _state(WEBSERVER) + , _state(WEBSERVICE) , _serializerImpl(*this, queueSize) , _deserialiserImpl(*this, queueSize) , _path() From 47697662e9c3d305054e99716d3c5e766bcb4937 Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 7 Nov 2024 08:29:36 +0000 Subject: [PATCH 04/12] [Tests/unit/core] : add 'test_websocket' --- Tests/unit/core/test_websocket.cpp | 1006 ++++++++++++++++++++++++++++ 1 file changed, 1006 insertions(+) create mode 100644 Tests/unit/core/test_websocket.cpp diff --git a/Tests/unit/core/test_websocket.cpp b/Tests/unit/core/test_websocket.cpp new file mode 100644 index 000000000..2442085b0 --- /dev/null +++ b/Tests/unit/core/test_websocket.cpp @@ -0,0 +1,1006 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#ifndef MODULE_NAME +#include "../Module.h" +#endif + +#include +#include +#include + +#include "../IPTestAdministrator.h" + +namespace Thunder { +namespace Tests { +namespace Core { + + class CustomSocketStream : public ::Thunder::Core::SocketStream { + public: + CustomSocketStream( + const SOCKET& socket + , const ::Thunder::Core::NodeId& localNode + , const uint16_t sendBufferSize + , const uint16_t receiveBufferSize + , const std::string& prefix + ) + : SocketStream(false, socket, localNode, sendBufferSize, receiveBufferSize) + , _prefix { prefix } + { + } + + CustomSocketStream( + const bool + , const ::Thunder::Core::NodeId& localNode + , const ::Thunder::Core::NodeId& remoteNode + , const uint16_t sendBufferSize + , const uint16_t receiveBufferSize + , const std::string& prefix + ) + : SocketStream(false, localNode, remoteNode, sendBufferSize, receiveBufferSize, sendBufferSize, receiveBufferSize) + , _prefix { prefix } + { + } + + ~CustomSocketStream() + { + std::cout.flush(); + } + + // Raw TCP data + int32_t Read(uint8_t buffer[], const uint16_t length) const override + { +#ifdef _VERBOSE + std::cout << std::dec <<__LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; +#endif + + int32_t count = SocketPort::Read(buffer, length); + +#ifdef _VERBOSE + if (count > 0) { + std::cout << " |--> buffer ( " << count << "/" << length << " ) = "; + for (int32_t index = 0; index < count; index++) { + std::cout << std::hex << static_cast(buffer[index]) << " "; + } + std::cout << "\n"; + } +#endif + + return count; + } + + // Raw TCP data + int32_t Write(const uint8_t buffer[], const uint16_t length) override + { +#ifdef _VERBOSE + std::cout << std::dec <<__LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; +#endif + + int32_t count = SocketPort::Write(buffer, length); + +#ifdef _VERBOSE + if (count > 0) { + std::cout << " |--> buffer ( " << count << "/"<< length <<" ) = "; + for (int32_t index = 0; index < count; index++) { + std::cout << std::hex << static_cast(buffer[index]) << " "; + } + std::cout << "\n"; + } +#endif + + return count; + } + + private: + + const std::string _prefix; + }; + + template + class WebSocketClient : public ::Thunder::Web::WebSocketClientType { + public : + + template + WebSocketClient( + const string& path + , const string& protocol + , const string& query + , const string& origin + , const bool binary + , const bool masking + , Args&&... args + ) + : ::Thunder::Web::WebSocketClientType(path, protocol, query, origin, binary, masking, /* */ std::forward(args)... /**/) + { + } + + ~WebSocketClient() override = default; + + // Non-idle then data available to send + bool IsIdle() const override { return _post.size() == 0; } + + // Allow for eventfull state updates in this class + void StateChange() override + { +#ifdef _VERBOSE + std::cout << std::dec << __LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; + + // Socket port open AND upgraded to WebSocket + std::cout << " |--> IsOpen() = " << this->IsOpen() << "\n"; + // Link will not accept new messages as they arrive + std::cout << " |--> IsSuspended() = " << this->IsSuspended() << "\n"; + // Socket has been closed (removed), link cannot receive new TCP data + std::cout << " |--> IsClosed() = " << this->IsClosed() << "\n"; + // Regular HTTP connection, no upgraded connection + std::cout << " |--> IsWebServer() = " << this->IsWebServer() << "\n"; + // Upgrade in progress + std::cout << " |--> IsUpgrading() = " << this->IsUpgrading() << "\n"; + // Upgraded connection + std::cout << " |--> IsWebSocket() = " << this->IsWebSocket() << "\n"; + // Finishing frame received + std::cout << " |--> IsCompleted() = " << this->IsCompleted() << "\n"; +#endif + } + + // Reflects payload, effective after upgrade + uint16_t SendData(uint8_t* dataFrame, const uint16_t maxSendSize) override + { +#ifdef _VERBOSE + std::cout << std::dec << __LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; +#endif + + size_t count = 0; + + if ( dataFrame != nullptr + && maxSendSize > 0 + && !IsIdle() + && ::Thunder::Web::WebSocketClientType::IsOpen() + && ::Thunder::Web::WebSocketClientType::IsWebSocket() // Redundant, covered by IsOpen + && ::Thunder::Web::WebSocketClientType::IsCompleted() + ) { + std::basic_string& message = _post.front(); + + count = std::min(message.size(), static_cast(maxSendSize)); + + /* void* */ memcpy(dataFrame, message.data(), count); + +#ifdef _VERBOSE + std::cout << " |--> dataFrame ( " << count << "/" << maxSendSize << " ) = "; + for (int32_t index = 0; index < count; index++) { + std::cout << std::hex << static_cast(dataFrame[index]) << " "; + } + std::cout << "\n"; +#endif + + if (count == message.size()) { + /* iterator */ _post.erase(_post.begin()); + } else { + /* this */ message.erase(0, count); + + // Trigger a call to SendData for remaining data + ::Thunder::Web::WebSocketClientType::Link().Trigger(); + } + } + + return count; + } + + // Reflects payload, effective after upgrade + uint16_t ReceiveData(uint8_t* dataFrame, const uint16_t receivedSize) override + { +#ifdef _VERBOSE + std::cout << std::dec << __LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; + + if (receivedSize > 0) { + std::cout << " |--> dataFrame ( " << receivedSize << " ) = "; + for (int32_t index = 0; index < receivedSize; index++) { + std::cout << std::hex << static_cast(dataFrame[index]) << " "; + } + std::cout << "\n"; + } +#endif + + // Echo the data in reverse order + + std::basic_string message{ dataFrame, receivedSize }; + + std::reverse(message.begin(), message.end()); + + return Submit(message) ? message.size() : 0; + } + + bool Submit(const std::basic_string& message) + { + size_t count = _post.size(); + + _post.emplace_back(message); + + ::Thunder::Web::WebSocketClientType::Link().Trigger(); + + return count < _post.size(); + } + + private: + + std::vector> _post; // Send message queue + }; + + template + class WebSocketServer : public ::Thunder::Web::WebSocketServerType { + public : + + // SocketServerType defines SocketHandler of type SocketListener. SocketListener triggers Accept on StateChange, effectively, calling SocketServerType::Accept(SOCKET, NodeId) which creates a WebSocketServer with these parameters + WebSocketServer(const SOCKET& socket, const ::Thunder::Core::NodeId remoteNode, ::Thunder::Core::SocketServerType>*) + // Initially this should be defined as a regular TCP socket + : ::Thunder::Web::WebSocketServerType(false /* binary*/, false /*masking */, socket, remoteNode, SENDBUFFERSIZE /* send buffer size */, RECEIVEBUFFERSIZE /* receive buffer size */, "WebSocketServerType") + { + } + + ~WebSocketServer() override = default; + + // Non-idle then data available to send + bool IsIdle() const override { return _post.size() == 0; } + + // Allow for eventfull state updates in this class + void StateChange() override + { +#ifdef _VERBOSE + std::cout << std::dec << __LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; + + // Socket port open AND upgraded to WebSocket + std::cout << " |--> IsOpen() = " << this->IsOpen() << "\n"; + // Link will not accept new messages as they arrive + std::cout << " |--> IsSuspended() = " << this->IsSuspended() << "\n"; + // Socket has been closed (removed), link cannot receive new TCP data + std::cout << " |--> IsClosed() = " << this->IsClosed() << "\n"; + // Regular HTTP connection, no upgraded connection + std::cout << " |--> IsWebServer() = " << this->IsWebServer() << "\n"; + // Upgrade in progress + std::cout << " |--> IsUpgrading() = " << this->IsUpgrading() << "\n"; + // Upgraded connection + std::cout << " |--> IsWebSocket() = " << this->IsWebSocket() << "\n"; + // Finishing frame received + std::cout << " |--> IsCompleted() = " << this->IsCompleted() << "\n"; +#endif + } + + // Reflects payload, effective after upgrade + uint16_t SendData(uint8_t* dataFrame, const uint16_t maxSendSize) override + { +#ifdef _VERBOSE + std::cout << std::dec << __LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; +#endif + + size_t count = 0; + + if ( dataFrame != nullptr + && maxSendSize > 0 + && !IsIdle() + && ::Thunder::Web::WebSocketServerType::IsOpen() + && ::Thunder::Web::WebSocketServerType::IsWebSocket() // Redundant, covered by IsOpen + && ::Thunder::Web::WebSocketServerType::IsCompleted() + ) { + std::basic_string& message = _post.front(); + + count = std::min(message.size(), static_cast(maxSendSize)); + + /* void* */ memcpy(dataFrame, message.data(), count); + +#ifdef _VERBOSE + std::cout << " |--> dataFrame (" << count << " ) = "; + for (int32_t index = 0; index < count; index++) { + std::cout << std::hex << static_cast(dataFrame[index]) << " "; + } + std::cout << "\n"; +#endif + + if (count == message.size()) { + /* iterator */ _post.erase(_post.begin()); + } else { + /* this */ message.erase(0, count); + + // Trigger a call to SendData for remaining data + ::Thunder::Web::WebSocketServerType::Link().Trigger(); + } + } + + return count; + } + + // Reflects payload, effective after upgrade + uint16_t ReceiveData(uint8_t* dataFrame, const uint16_t receivedSize) override { +#ifdef _VERBOSE + std::cout << std::dec << __LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; +#endif + + if (receivedSize > 0) { + _response.emplace_back(std::basic_string{ dataFrame, receivedSize }); + +#ifdef _VERBOSE + std::cout << " |--> dataFrame ( " << receivedSize << " ) = "; + for (int32_t index = 0; index < receivedSize; index++) { + std::cout << std::hex << static_cast(dataFrame[index]) << " "; + } + std::cout << "\n"; +#endif + } + + return receivedSize; + } + + // Put data in the queue to send (to the (connected) client) + bool Submit(const std::basic_string& message) + { + size_t count = _post.size(); + + _post.emplace_back(message); + + // Trigger a call to SendData + ::Thunder::Web::WebSocketServerType::Link().Trigger(); + + return count < _post.size(); + } + + std::basic_string Response() + { +#ifdef _VERBOSE + std::cout << std::dec << __LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; +#endif + + std::basic_string message; + + if (_response.size() > 0) { + message = _response.front(); + _response.erase(_response.begin()); + +#ifdef _VERBOSE + std::cout << " |--> message ( " << message.size() << " ) = "; + for (int32_t index = 0; index < message.size(); index++) { + std::cout << std::hex << static_cast(message[index]) << " "; + } + std::cout << "\n"; +#endif + } + + return message; + } + + private: + + std::vector> _post; // Send message queue + std::vector> _response; // Receive message queue + }; + + TEST(WebSocket, DISABLED_OpeningServerPort) + { + const TCHAR localHostName[] {"127.0.0.1"}; + + constexpr uint16_t tcpServerPort {12345}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {1024}; + constexpr uint16_t receiveBufferSize {1024}; + + constexpr uint32_t maxWaitTimeMs = 4000; + + const ::Thunder::Core::NodeId localNode {localHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + // This is a listening socket as result of using SocketServerType which enables listening + ::Thunder::Core::SocketServerType> server(localNode /* listening node*/); + + ASSERT_EQ(server.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + SleepMs(maxWaitTimeMs); + + // Obtain the endpoint at the server side for each (remotely) connected client + auto it = server.Clients(); + + if (it.Next()) { + // Unless a client has send an upgrade request we cannot send data out although we might be calling WebSocket functionality + + if (it.Client()->IsOpen()) { + // No data should be transferred to the remote client + } + } + + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(server.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + } + + TEST(WebSocket, DISABLED_OpeningClientPort) + { + const std::string webSocketURIPath; // HTTP URI part, empty path allowed + const std::string webSocketProtocol; // Optional HTTP field, WebSocket SubProtocol, ie, Sec-WebSocket-Protocol + const std::string webSocketURIQuery; // HTTP URI part, absent query allowe + const std::string webSocketOrigin; // Optional, set by browser clients + constexpr bool binary {false}; // Flag to indicate WebSocket opcode 0x1 (test frame) or 0x2 (binary frame) + constexpr bool masking {true}; // Flag set by client to enable masking + + const TCHAR remoteHostName[] {"127.0.0.1"}; + + constexpr uint16_t tcpServerPort {12345}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + constexpr bool rawSocket {false}; + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {1024}; + constexpr uint16_t receiveBufferSize {1024}; + + constexpr uint32_t maxWaitTimeMs = 4000; + + const ::Thunder::Core::NodeId remoteNode {remoteHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + WebSocketClient client(webSocketURIPath, webSocketProtocol, webSocketURIQuery, webSocketOrigin, false, true, rawSocket, remoteNode.AnyInterface(), remoteNode, sendBufferSize, receiveBufferSize, "WebSocketClient"); + + SleepMs(maxWaitTimeMs); + + ASSERT_EQ(client.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(client.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + } + + TEST(WebSocket, DISABLED_UnsecuredSocketUpgrade) + { + const TCHAR hostName[] {"127.0.0.1"}; + + // Some aliases + const auto& remoteHostName = hostName; + const auto& localHostName = hostName; + + constexpr uint16_t tcpServerPort {12345}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {1024}; + constexpr uint16_t receiveBufferSize {1024}; + + constexpr uint32_t initHandshakeValue = 0, maxWaitTime = 8, maxWaitTimeMs = 8000, maxInitTime = 2000; + constexpr uint8_t maxRetries = 10; + + IPTestAdministrator::Callback callback_child = [&](IPTestAdministrator& testAdmin) { + const std::string webSocketURIPath; // HTTP URI part, empty path allowed + const std::string webSocketProtocol; // Optional HTTP field, WebSocket SubProtocol, ie, Sec-WebSocket-Protocol + const std::string webSocketURIQuery; // HTTP URI part, absent query allowe + const std::string webSocketOrigin; // Optional, set by browser clients + constexpr bool binary {false}; // Flag to indicate WebSocket opcode 0x1 (test frame) or 0x2 (binary frame) + constexpr bool masking {true}; // Flag set by client to enable masking + + constexpr bool rawSocket {false}; + + const ::Thunder::Core::NodeId remoteNode {remoteHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + WebSocketClient client(webSocketURIPath, webSocketProtocol, webSocketURIQuery, webSocketOrigin, false, true, rawSocket, remoteNode.AnyInterface(), remoteNode, sendBufferSize, receiveBufferSize, "WebSocketClient"); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + ASSERT_EQ(client.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(client.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator::Callback callback_parent = [&](IPTestAdministrator& testAdmin) { + const ::Thunder::Core::NodeId localNode {localHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + // This is a listening socket as result of using SocketServerType which enables listening + ::Thunder::Core::SocketServerType> server(localNode /* listening node*/); + + ASSERT_EQ(server.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + // A small delay so the child can be set up +// SleepMs(maxInitTime); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(server.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator testAdmin(callback_parent, callback_child, initHandshakeValue, maxWaitTime); + + // Code after this line is executed by both parent and child + + ::Thunder::Core::Singleton::Dispose(); + } + + TEST(WebSocket, DISABLED_UnsecuredSocketServerPingClientPong) + { + const TCHAR hostName[] {"127.0.0.1"}; + + // Some aliases + const auto& remoteHostName = hostName; + const auto& localHostName = hostName; + + constexpr uint16_t tcpServerPort {12346}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {1024}; + constexpr uint16_t receiveBufferSize {1024}; + + constexpr uint32_t initHandshakeValue = 0, maxWaitTime = 8, maxWaitTimeMs = 8000, maxInitTime = 2000; + constexpr uint8_t maxRetries = 10; + + IPTestAdministrator::Callback callback_child = [&](IPTestAdministrator& testAdmin) { + const std::string webSocketURIPath; // HTTP URI part, empty path allowed + const std::string webSocketProtocol; // Optional HTTP field, WebSocket SubProtocol, ie, Sec-WebSocket-Protocol + const std::string webSocketURIQuery; // HTTP URI part, absent query allowe + const std::string webSocketOrigin; // Optional, set by browser clients + constexpr bool binary {false}; // Flag to indicate WebSocket opcode 0x1 (test frame) or 0x2 (binary frame) + constexpr bool masking {true}; // Flag set by client to enable masking + + constexpr bool rawSocket {false}; + + const ::Thunder::Core::NodeId remoteNode {remoteHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + WebSocketClient client(webSocketURIPath, webSocketProtocol, webSocketURIQuery, webSocketOrigin, false, true, rawSocket, remoteNode.AnyInterface(), remoteNode, sendBufferSize, receiveBufferSize, "WebSocketClient"); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + ASSERT_EQ(client.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + // Avoid premature shutdown() at the other side + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(client.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator::Callback callback_parent = [&](IPTestAdministrator& testAdmin) { + const ::Thunder::Core::NodeId localNode {localHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + // This is a listening socket as result of using SocketServerType which enables listening + ::Thunder::Core::SocketServerType> server(localNode /* listening node*/); + + ASSERT_EQ(server.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + // A small delay so the child can be set up +// SleepMs(maxInitTime); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + // Obtain the endpoint at the server side for each (remotely) connected client + auto it = server.Clients(); + + if (it.Next()) { + // Unless a client has send an upgrade request we cannot send data out although we might be calling WebSocket functionality + + if (it.Client()->IsOpen()) { + /* void */ it.Client()->Ping(); + } + } + + // Allow some time to receive the PONG response + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(server.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator testAdmin(callback_parent, callback_child, initHandshakeValue, maxWaitTime); + + // Code after this line is executed by both parent and child + + ::Thunder::Core::Singleton::Dispose(); + } + + TEST(WebSocket, DISABLED_UnsecuredSocketServerUnsollicitedPong) + { + const TCHAR hostName[] {"127.0.0.1"}; + + // Some aliases + const auto& remoteHostName = hostName; + const auto& localHostName = hostName; + + constexpr uint16_t tcpServerPort {12346}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {1024}; + constexpr uint16_t receiveBufferSize {1024}; + + constexpr uint32_t initHandshakeValue = 0, maxWaitTime = 8, maxWaitTimeMs = 8000, maxInitTime = 2000; + constexpr uint8_t maxRetries = 10; + + IPTestAdministrator::Callback callback_child = [&](IPTestAdministrator& testAdmin) { + const std::string webSocketURIPath; // HTTP URI part, empty path allowed + const std::string webSocketProtocol; // Optional HTTP field, WebSocket SubProtocol, ie, Sec-WebSocket-Protocol + const std::string webSocketURIQuery; // HTTP URI part, absent query allowe + const std::string webSocketOrigin; // Optional, set by browser clients + constexpr bool binary {false}; // Flag to indicate WebSocket opcode 0x1 (test frame) or 0x2 (binary frame) + constexpr bool masking {true}; // Flag set by client to enable masking + + constexpr bool rawSocket {false}; + + const ::Thunder::Core::NodeId remoteNode {remoteHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + WebSocketClient client(webSocketURIPath, webSocketProtocol, webSocketURIQuery, webSocketOrigin, false, true, rawSocket, remoteNode.AnyInterface(), remoteNode, sendBufferSize, receiveBufferSize, "WebSocketClient"); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + ASSERT_EQ(client.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + // Allow some time to receive the PONG + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(client.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator::Callback callback_parent = [&](IPTestAdministrator& testAdmin) { + const ::Thunder::Core::NodeId localNode {localHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + // This is a listening socket as result of using SocketServerType which enables listening + ::Thunder::Core::SocketServerType> server(localNode /* listening node*/); + + ASSERT_EQ(server.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + // A small delay so the child can be set up +// SleepMs(maxInitTime); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + // Obtain the endpoint at the server side for each (remotely) connected client + auto it = server.Clients(); + + if (it.Next()) { + // Unless a client has send an upgrade request we cannot send data out although we might be calling WebSocket functionality + + if (it.Client()->IsOpen()) { + /* void */ it.Client()->Pong(); + } + } + + // Avoid premature shutdown() at the receiving side + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(server.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator testAdmin(callback_parent, callback_child, initHandshakeValue, maxWaitTime); + + // Code after this line is executed by both parent and child + + ::Thunder::Core::Singleton::Dispose(); + } + + TEST(WebSocket, DISABLED_UnsecuredSocketClientUnsollicitedPong) + { + const TCHAR hostName[] {"127.0.0.1"}; + + // Some aliases + const auto& remoteHostName = hostName; + const auto& localHostName = hostName; + + constexpr uint16_t tcpServerPort {12346}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {1024}; + constexpr uint16_t receiveBufferSize {1024}; + + constexpr uint32_t initHandshakeValue = 0, maxWaitTime = 8, maxWaitTimeMs = 8000, maxInitTime = 2000; + constexpr uint8_t maxRetries = 10; + + IPTestAdministrator::Callback callback_child = [&](IPTestAdministrator& testAdmin) { + const std::string webSocketURIPath; // HTTP URI part, empty path allowed + const std::string webSocketProtocol; // Optional HTTP field, WebSocket SubProtocol, ie, Sec-WebSocket-Protocol + const std::string webSocketURIQuery; // HTTP URI part, absent query allowe + const std::string webSocketOrigin; // Optional, set by browser clients + constexpr bool binary {false}; // Flag to indicate WebSocket opcode 0x1 (test frame) or 0x2 (binary frame) + constexpr bool masking {true}; // Flag set by client to enable masking + + constexpr bool rawSocket {false}; + + const ::Thunder::Core::NodeId remoteNode {remoteHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + WebSocketClient client(webSocketURIPath, webSocketProtocol, webSocketURIQuery, webSocketOrigin, false, true, rawSocket, remoteNode.AnyInterface(), remoteNode, sendBufferSize, receiveBufferSize, "WebSocketClient"); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + ASSERT_EQ(client.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + client.Pong(); + + // Avoid premature shutdown() at the other side + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(client.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator::Callback callback_parent = [&](IPTestAdministrator& testAdmin) { + const ::Thunder::Core::NodeId localNode {localHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + // This is a listening socket as result of using SocketServerType which enables listening + ::Thunder::Core::SocketServerType> server(localNode /* listening node*/); + + ASSERT_EQ(server.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + // A small delay so the child can be set up +// SleepMs(maxInitTime); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + // Obtain the endpoint at the server side for each (remotely) connected client + auto it = server.Clients(); + + // Allow some time to receive the PONG + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(server.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator testAdmin(callback_parent, callback_child, initHandshakeValue, maxWaitTime); + + // Code after this line is executed by both parent and child + + ::Thunder::Core::Singleton::Dispose(); + } + + TEST(WebSocket, DISABLED_UnsecuredSocketDataExchange) + { + const TCHAR hostName[] {"127.0.0.1"}; + + // Some aliases + const auto& remoteHostName = hostName; + const auto& localHostName = hostName; + + constexpr uint16_t tcpServerPort {12346}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {1024}; + constexpr uint16_t receiveBufferSize {1024}; + + constexpr uint32_t initHandshakeValue = 0, maxWaitTime = 8, maxWaitTimeMs = 8000, maxInitTime = 2000; + constexpr uint8_t maxRetries = 10; + + IPTestAdministrator::Callback callback_child = [&](IPTestAdministrator& testAdmin) { + const std::string webSocketURIPath; // HTTP URI part, empty path allowed + const std::string webSocketProtocol; // Optional HTTP field, WebSocket SubProtocol, ie, Sec-WebSocket-Protocol + const std::string webSocketURIQuery; // HTTP URI part, absent query allowe + const std::string webSocketOrigin; // Optional, set by browser clients + constexpr bool binary {false}; // Flag to indicate WebSocket opcode 0x1 (test frame) or 0x2 (binary frame) + constexpr bool masking {true}; // Flag set by client to enable masking + + constexpr bool rawSocket {false}; + + const ::Thunder::Core::NodeId remoteNode {remoteHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + WebSocketClient client(webSocketURIPath, webSocketProtocol, webSocketURIQuery, webSocketOrigin, false, true, rawSocket, remoteNode.AnyInterface(), remoteNode, sendBufferSize, receiveBufferSize, "WebSocketClient"); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + ASSERT_EQ(client.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + // Avoid premature shutdown() at the other side + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(client.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator::Callback callback_parent = [&](IPTestAdministrator& testAdmin) { + const ::Thunder::Core::NodeId localNode {localHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + // This is a listening socket as result of using SocketServerType which enables listening + ::Thunder::Core::SocketServerType> server(localNode /* listening node*/); + + ASSERT_EQ(server.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + // A small delay so the child can be set up +// SleepMs(maxInitTime); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + // Obtain the endpoint at the server side for each (remotely) connected client + auto it = server.Clients(); + + constexpr uint8_t data[] = { 0xF, 0xE, 0xD, 0xC, 0xB, 0xA, 0x9, 0x8, 0x7, 0x6, 0x5, 0x3, 0x2, 0x1, 0x0 }; + + if (it.Next()) { + // Unless a client has send an upgrade request we cannot send data out although we might be calling WebSocket functionality + + if (it.Client()->IsOpen()) { + /* bool */ it.Client()->Submit(std::basic_string{ data, sizeof(data) }); + } + } + + // Allow some time to receive the response + SleepMs(maxWaitTimeMs); + + std::basic_string response{ data, sizeof(data) }; + std::reverse(response.begin(), response.end()); + + // A simple poll to keep it simple + EXPECT_TRUE( it.IsValid() + && (it.Client()->Response() == response) + ); + + EXPECT_EQ(server.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator testAdmin(callback_parent, callback_child, initHandshakeValue, maxWaitTime); + + // Code after this line is executed by both parent and child + + ::Thunder::Core::Singleton::Dispose(); + } + + TEST(WebSocket, UnsecuredSocketMultiFrameDataExchange) + { + const TCHAR hostName[] {"127.0.0.1"}; + + // Some aliases + const auto& remoteHostName = hostName; + const auto& localHostName = hostName; + + constexpr uint16_t tcpServerPort {12346}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {75}; + constexpr uint16_t receiveBufferSize {75}; + + constexpr uint32_t initHandshakeValue = 0, maxWaitTime = 8, maxWaitTimeMs = 8000, maxInitTime = 2000; + constexpr uint8_t maxRetries = 10; + + constexpr uint16_t nagglesTimeoutMs = 250; // Typical is 200 milliseconds + + IPTestAdministrator::Callback callback_child = [&](IPTestAdministrator& testAdmin) { + const std::string webSocketURIPath; // HTTP URI part, empty path allowed + const std::string webSocketProtocol; // Optional HTTP field, WebSocket SubProtocol, ie, Sec-WebSocket-Protocol + const std::string webSocketURIQuery; // HTTP URI part, absent query allowe + const std::string webSocketOrigin; // Optional, set by browser clients + constexpr bool binary {false}; // Flag to indicate WebSocket opcode 0x1 (test frame) or 0x2 (binary frame) + constexpr bool masking {true}; // Flag set by client to enable masking + + constexpr bool rawSocket {false}; + + const ::Thunder::Core::NodeId remoteNode {remoteHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + WebSocketClient client(webSocketURIPath, webSocketProtocol, webSocketURIQuery, webSocketOrigin, false, true, rawSocket, remoteNode.AnyInterface(), remoteNode, sendBufferSize, receiveBufferSize, "WebSocketClient"); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + ASSERT_EQ(client.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + +#ifdef _VERBOSE + std::cout << std::dec <<__LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; + std::cout << " |--> SendBufferSize = " << client.Link().SendBufferSize() << "\n"; + std::cout << " |--> ReceiveBufferSize = " << client.Link().ReceiveBufferSize() << "\n"; + std::cout << " |--> SocketSendBufferSize = " << client.Link().SocketSendBufferSize() << "\n"; + std::cout << " |--> SocketReceiveBufferSize = " << client.Link().SocketReceiveBufferSize() << "\n"; +#endif + + // Avoid premature shutdown() at the other side + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(client.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator::Callback callback_parent = [&](IPTestAdministrator& testAdmin) { + const ::Thunder::Core::NodeId localNode {localHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + // This is a listening socket as result of using SocketServerType which enables listening + ::Thunder::Core::SocketServerType> server(localNode /* listening node*/); + + ASSERT_EQ(server.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + // A small delay so the child can be set up +// SleepMs(maxInitTime); + + EXPECT_EQ(testAdmin.Signal(initHandshakeValue, maxRetries), ::Thunder::Core::ERROR_NONE); + + EXPECT_EQ(testAdmin.Wait(initHandshakeValue), ::Thunder::Core::ERROR_NONE); + + // Obtain the endpoint at the server side for each (remotely) connected client + auto it = server.Clients(); + + // Do not use '\0' as a marker as std::basic_strng<> assumes it is an end of string + + constexpr uint8_t data[] = { 0xF, 0xE, 0xD, 0xC, 0xB, 0xA, 0x9, 0x8, 0x7, 0x6, 0x5, 0x3, 0x2, 0x1, 0x10 }; + + std::basic_string message; + + if (it.Next()) { + // Unless a client has send an upgrade request we cannot send data out although we might be calling WebSocket functionality + + if (it.Client()->IsOpen()) { + // Construct a message larger than the buffer size to force use of continuation frames + size_t count = (it.Client()->Link().SendBufferSize() / sizeof(data) + 1 ) * sizeof(data); + + ASSERT_LE(count, message.max_size()); + + message.resize(count); + + for (size_t index = 0; index < count; index += sizeof(data) ) { + message.replace(index, sizeof(data), data); + } + + /* bool */ it.Client()->Submit(std::basic_string{ message.data(), count }); + } + } + + // Allow some time to receive the response + SleepMs(maxWaitTimeMs); + + std::reverse(message.begin(), message.end()); + + std::basic_string response; + + response.reserve( message.size() ); + + // A simple poll to keep it simple + for (int8_t retry = 0; retry < maxRetries; ++retry) { + SleepMs(nagglesTimeoutMs); // Naggle's typical delay, perhaps a bit more + + if (it.IsValid()) { + response = it.Client()->Response() + response; + } + } + + EXPECT_TRUE( response.size() == message.size() + && response == message + ); + + EXPECT_EQ(server.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + }; + + IPTestAdministrator testAdmin(callback_parent, callback_child, initHandshakeValue, maxWaitTime); + + // Code after this line is executed by both parent and child + + ::Thunder::Core::Singleton::Dispose(); + } + +} // Core +} // Tests +} // Thunder From 45bfec56f7bc95c1dd26b676b1d9a7b3dae82d16 Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:28:58 +0000 Subject: [PATCH 05/12] [Tests/unit/core] : Add missing VERBOSE preprocessor directive to 'test_websocket' --- Tests/unit/core/test_websocket.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/unit/core/test_websocket.cpp b/Tests/unit/core/test_websocket.cpp index 2442085b0..794875e69 100644 --- a/Tests/unit/core/test_websocket.cpp +++ b/Tests/unit/core/test_websocket.cpp @@ -62,7 +62,9 @@ namespace Core { ~CustomSocketStream() { +#ifdef _VERBOSE std::cout.flush(); +#endif } // Raw TCP data From a206e194452e7a098eb937b3865bc2de1314a2c2 Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:42:09 +0000 Subject: [PATCH 06/12] [Tests/unit/core] : Add 'SecureSocketPort' tests to 'test_websocket' --- Tests/unit/core/test_websocket.cpp | 145 ++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/Tests/unit/core/test_websocket.cpp b/Tests/unit/core/test_websocket.cpp index 794875e69..a2cb1f4f7 100644 --- a/Tests/unit/core/test_websocket.cpp +++ b/Tests/unit/core/test_websocket.cpp @@ -391,6 +391,75 @@ namespace Core { std::vector> _response; // Receive message queue }; + class CustomSecureSocketStream : public ::Thunder::Crypto::SecureSocketPort { + private : + + // Validat eclient certificate + class Validator : public ::Thunder::Crypto::SecureSocketPort::IValidator { + public: + + Validator() = default; + ~Validator() = default; + + bool Validate(const Certificate& certificate) const override { + // Print certificate properties +#ifdef _VERBOSE + std::cout << std::dec <<__LINE__ << " : " << __PRETTY_FUNCTION__ << "\n"; + std::cout << " |--> Issuer = " << certificate.Issuer() << "\n"; + std::cout << " |--> Subject = " << certificate.Subject() << "\n"; + std::cout << " |--> Valid from = " << certificate.ValidFrom().ToRFC1123() << "\n"; + std::cout << " |--> Valid until = " << certificate.ValidTill().ToRFC1123() << "\n"; +#endif + return true; // Always accept + } + }; + + public : + + // In essence, all parameters to SecureSocket are passed to a base class SocketPort + CustomSecureSocketStream( + const SOCKET& socket + , const ::Thunder::Core::NodeId& localNode + , const uint16_t sendBufferSize + , const uint16_t receiveBufferSize + , const std::string& prefix + ) + : ::Thunder::Crypto::SecureSocketPort(::Thunder::Core::SocketPort::STREAM, socket, localNode, sendBufferSize, receiveBufferSize) + , _prefix{ prefix } + , _validator{} + { + // Validate custom (sefl signed) certificates + uint32_t result = Callback(&_validator); + } + + CustomSecureSocketStream( + const bool + , const ::Thunder::Core::NodeId& localNode + , const ::Thunder::Core::NodeId& remoteNode + , const uint16_t sendBufferSize + , const uint16_t receiveBufferSize + , const std::string& prefix + ) + : ::Thunder::Crypto::SecureSocketPort(::Thunder::Core::SocketPort::STREAM, localNode, remoteNode, sendBufferSize, receiveBufferSize, sendBufferSize, receiveBufferSize) + , _prefix{ prefix } + , _validator{} + { + // Validate custom (self signed) client certificates + uint32_t result = Callback(&_validator); + } + + ~CustomSecureSocketStream() + { + #ifdef _VERBOSE + std::cout.flush(); +#endif + } + + private: + const std::string _prefix; + Validator _validator; + }; + TEST(WebSocket, DISABLED_OpeningServerPort) { const TCHAR localHostName[] {"127.0.0.1"}; @@ -874,7 +943,7 @@ namespace Core { ::Thunder::Core::Singleton::Dispose(); } - TEST(WebSocket, UnsecuredSocketMultiFrameDataExchange) + TEST(WebSocket, DISABLED_UnsecuredSocketMultiFrameDataExchange) { const TCHAR hostName[] {"127.0.0.1"}; @@ -1003,6 +1072,80 @@ namespace Core { ::Thunder::Core::Singleton::Dispose(); } + TEST(WebSocket, DISABLED_OpeningSecuredServerPort) + { + const TCHAR localHostName[] {"127.0.0.1"}; + + constexpr uint16_t tcpServerPort {12345}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {1024}; + constexpr uint16_t receiveBufferSize {1024}; + + constexpr uint32_t maxWaitTimeMs = 4000; + + const ::Thunder::Core::NodeId localNode {localHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + // This is a listening socket as result of using SocketServerType which enables listening + ::Thunder::Core::SocketServerType> server(localNode /* listening node*/); + + ASSERT_EQ(server.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + +// SleepMs(maxWaitTimeMs); + + // Obtain the endpoint at the server side for each (remotely) connected client + auto it = server.Clients(); + + if (it.Next()) { + // Unless a client has send an upgrade request we cannot send data out although we might be calling WebSocket functionality + if (it.Client()->IsOpen()) { + // No data should be transferred to the remote client + } else { + } + } + + SleepMs(4*maxWaitTimeMs); + + + EXPECT_EQ(server.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + } + + TEST(WebSocket, OpeningSecuredClientPort) + { + const std::string webSocketURIPath; // HTTP URI part, empty path allowed + const std::string webSocketProtocol; // Optional HTTP field, WebSocket SubProtocol, ie, Sec-WebSocket-Protocol + const std::string webSocketURIQuery; // HTTP URI part, absent query allowe + const std::string webSocketOrigin; // Optional, set by browser clients + constexpr bool binary {false}; // Flag to indicate WebSocket opcode 0x1 (test frame) or 0x2 (binary frame) + constexpr bool masking {true}; // Flag set by client to enable masking + + const TCHAR remoteHostName[] {"127.0.0.1"}; + + constexpr uint16_t tcpServerPort {12345}; // TCP, default 80 or 443 (SSL) + constexpr uint32_t tcpProtocol {0}; // HTTP or HTTPS but can only be set on raw sockets + + constexpr bool rawSocket {false}; + + // The minimum size is determined by the HTTP upgrade process. The limit here is above that threshold. + constexpr uint16_t sendBufferSize {1024}; + constexpr uint16_t receiveBufferSize {1024}; + + constexpr uint32_t maxWaitTimeMs = 4000; + + const ::Thunder::Core::NodeId remoteNode {remoteHostName, tcpServerPort, ::Thunder::Core::NodeId::TYPE_IPV4, tcpProtocol}; + + WebSocketClient client(webSocketURIPath, webSocketProtocol, webSocketURIQuery, webSocketOrigin, false, true, rawSocket, remoteNode.AnyInterface(), remoteNode, sendBufferSize, receiveBufferSize, "WebSocketClient"); + +// SleepMs(maxWaitTimeMs); + + ASSERT_EQ(client.Open(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + + SleepMs(maxWaitTimeMs); + + EXPECT_EQ(client.Close(maxWaitTimeMs), ::Thunder::Core::ERROR_NONE); + } + } // Core } // Tests } // Thunder From 426161696810252a8730c26277f2e14bbde259e2 Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:15:08 +0000 Subject: [PATCH 07/12] [Source/crypto/SecureSocketPort] : Initialize when context and SSL structure are not yet contructed, and, improve SSL read and write use - SSL read and write methods should only be used if the handshake is successfull - Do not return error values for SSL read and write methods as number of read or written bytes --- Source/cryptalgo/SecureSocketPort.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Source/cryptalgo/SecureSocketPort.cpp b/Source/cryptalgo/SecureSocketPort.cpp index 764b94398..6188954aa 100644 --- a/Source/cryptalgo/SecureSocketPort.cpp +++ b/Source/cryptalgo/SecureSocketPort.cpp @@ -151,16 +151,25 @@ uint32_t SecureSocketPort::Handler::Initialize() { } int32_t SecureSocketPort::Handler::Read(uint8_t buffer[], const uint16_t length) const { - int32_t result = SSL_read(static_cast(_ssl), buffer, length); + int32_t result = 0; - if (_handShaking != CONNECTED) { - const_cast(*this).Update(); + if (_handShaking == CONNECTED) { + int value = SSL_read(static_cast(_ssl), buffer, length); + result = (value > 0 ? value : 0); } + return (result); } int32_t SecureSocketPort::Handler::Write(const uint8_t buffer[], const uint16_t length) { - return (SSL_write(static_cast(_ssl), buffer, length)); + int32_t result = 0; + + if (_handShaking == CONNECTED) { + int value = (SSL_write(static_cast(_ssl), buffer, length)); + result = (value > 0 ? value : 0); + } + + return (result); } From 4ddd69fb8fda90a4b8687b62cddb3ade83a14c53 Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:40:17 +0000 Subject: [PATCH 08/12] [Source/crypto/SecureSocketPort] : (Re)initialize if socket has been opened but context has not been contructed. --- Source/cryptalgo/SecureSocketPort.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/cryptalgo/SecureSocketPort.h b/Source/cryptalgo/SecureSocketPort.h index 280a44ae3..052333263 100644 --- a/Source/cryptalgo/SecureSocketPort.h +++ b/Source/cryptalgo/SecureSocketPort.h @@ -105,10 +105,14 @@ namespace Crypto { // Signal a state change, Opened, Closed or Accepted void StateChange() override { + if (IsOpen() && _context != nullptr) { // Initialize may not yet have happened + Initialize(); + } - ASSERT(_context != nullptr); - Update(); - }; + if (_ssl != nullptr) { + Update(); + } + }; inline uint32_t Callback(IValidator* callback) { uint32_t result = Core::ERROR_ILLEGAL_STATE; From 7916e61feaa02a4a9942203219e350ae2f56990c Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:43:21 +0000 Subject: [PATCH 09/12] [Source/crypto/SecureSocketPort] : Context settings do not (necessarily) propagate to SSL structure after construction. - Context should exist - Options should be set --- Source/cryptalgo/SecureSocketPort.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Source/cryptalgo/SecureSocketPort.cpp b/Source/cryptalgo/SecureSocketPort.cpp index 6188954aa..73f216c74 100644 --- a/Source/cryptalgo/SecureSocketPort.cpp +++ b/Source/cryptalgo/SecureSocketPort.cpp @@ -135,16 +135,21 @@ uint32_t SecureSocketPort::Handler::Initialize() { _context = SSL_CTX_new(TLS_method()); - _ssl = SSL_new(static_cast(_context)); - SSL_set_fd(static_cast(_ssl), static_cast(*this).Descriptor()); - SSL_CTX_set_options(static_cast(_context), SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); + if (_context != nullptr) { + if ( // Returns bit-mask after adding options + ((SSL_CTX_set_options(static_cast(_context), SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3) & (SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3)) == (SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3)) + && // Trust the same certificates as any other application + (SSL_CTX_set_default_verify_paths(static_cast(_context)) == 1) + ) { + _ssl = SSL_new(static_cast(_context)); - // Trust the same certificates as any other application - if (SSL_CTX_set_default_verify_paths(static_cast(_context)) == 1) { - success = Core::SocketPort::Initialize(); - } else { - TRACE_L1("OpenSSL failed to load certificate store"); - success = Core::ERROR_GENERAL; + SSL_set_fd(static_cast(_ssl), static_cast(*this).Descriptor()); + + success = Core::SocketPort::Initialize(); + } else { + TRACE_L1("OpenSSL failed to set protocol level or load certificate store"); + success = Core::ERROR_GENERAL; + } } return success; From 48b267b3f69691f09df3943726ae076ab28688e1 Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:59:13 +0000 Subject: [PATCH 10/12] [Source/crypto/SecureSocketPort] : check return value for setting hostname. --- Source/cryptalgo/SecureSocketPort.cpp | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Source/cryptalgo/SecureSocketPort.cpp b/Source/cryptalgo/SecureSocketPort.cpp index 73f216c74..9dbabc840 100644 --- a/Source/cryptalgo/SecureSocketPort.cpp +++ b/Source/cryptalgo/SecureSocketPort.cpp @@ -226,29 +226,26 @@ void SecureSocketPort::Handler::ValidateHandShake() { } void SecureSocketPort::Handler::Update() { - if (IsOpen() == true) { - int result; - + if (IsOpen() == true && _ssl != nullptr) { if (_handShaking == IDLE) { - SSL_set_tlsext_host_name(static_cast(_ssl), RemoteNode().HostName().c_str()); - result = SSL_connect(static_cast(_ssl)); - if (result == 1) { + int result{0}; + + if( (result = SSL_set_tlsext_host_name(static_cast(_ssl), RemoteNode().HostName().c_str()) == 1) + && (result = SSL_connect(static_cast(_ssl)) == 1) + ) { ValidateHandShake(); - } - else { + } else { // Support non-blocking SocketPorts, result taken from SSL_connect result = SSL_get_error(static_cast(_ssl), result); if ((result == SSL_ERROR_WANT_READ) || (result == SSL_ERROR_WANT_WRITE)) { _handShaking = EXCHANGE; } } - } - else if (_handShaking == EXCHANGE) { + } else if (_handShaking == EXCHANGE) { if (SSL_do_handshake(static_cast(_ssl)) == 1) { ValidateHandShake(); } } - } - else if (_ssl != nullptr) { + } else if (_ssl != nullptr) { _handShaking = IDLE; _parent.StateChange(); } From 62f635d4a07e4dbe673962b089442c72ddbf0e44 Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:22:43 +0000 Subject: [PATCH 11/12] [Source/crypto/SecureSocket] : improve return value checking in 'Handler::Initialize' --- Source/cryptalgo/SecureSocketPort.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Source/cryptalgo/SecureSocketPort.cpp b/Source/cryptalgo/SecureSocketPort.cpp index 9dbabc840..5299331d3 100644 --- a/Source/cryptalgo/SecureSocketPort.cpp +++ b/Source/cryptalgo/SecureSocketPort.cpp @@ -131,20 +131,16 @@ SecureSocketPort::Handler::~Handler() { } uint32_t SecureSocketPort::Handler::Initialize() { - uint32_t success = Core::ERROR_NONE; + uint32_t success = Core::ERROR_GENERAL; - _context = SSL_CTX_new(TLS_method()); - - if (_context != nullptr) { + if ((_context = SSL_CTX_new(TLS_method())) != nullptr) { if ( // Returns bit-mask after adding options ((SSL_CTX_set_options(static_cast(_context), SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3) & (SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3)) == (SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3)) && // Trust the same certificates as any other application (SSL_CTX_set_default_verify_paths(static_cast(_context)) == 1) - ) { - _ssl = SSL_new(static_cast(_context)); - - SSL_set_fd(static_cast(_ssl), static_cast(*this).Descriptor()); - + && ((_ssl = SSL_new(static_cast(_context))) != nullptr) + && (SSL_set_fd(static_cast(_ssl), static_cast(*this).Descriptor()) == 1) + ) { success = Core::SocketPort::Initialize(); } else { TRACE_L1("OpenSSL failed to set protocol level or load certificate store"); From feef85771357625aef28f2c1c70eb2bb4dd17d5b Mon Sep 17 00:00:00 2001 From: msieben <4319079+msieben@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:34:33 +0000 Subject: [PATCH 12/12] [Source/crypto/SecureSocket] : correct check for invalid context --- Source/cryptalgo/SecureSocketPort.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/cryptalgo/SecureSocketPort.h b/Source/cryptalgo/SecureSocketPort.h index 052333263..49c5d5ed1 100644 --- a/Source/cryptalgo/SecureSocketPort.h +++ b/Source/cryptalgo/SecureSocketPort.h @@ -105,7 +105,7 @@ namespace Crypto { // Signal a state change, Opened, Closed or Accepted void StateChange() override { - if (IsOpen() && _context != nullptr) { // Initialize may not yet have happened + if (IsOpen() && _context == nullptr) { // Initialize may not yet have happened Initialize(); }