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();
}