Tunnel implementation 20/304920/28
authorKrzysztof Jackiewicz <k.jackiewicz@samsung.com>
Mon, 18 Dec 2023 13:52:38 +0000 (14:52 +0100)
committerKrzysztof Jackiewicz <k.jackiewicz@samsung.com>
Tue, 5 Mar 2024 11:47:10 +0000 (12:47 +0100)
Manual and automated tests included.

Change-Id: Ie921f938b55a770a76cd68d45b888b6665807c46

21 files changed:
CMakeLists.txt
packaging/webauthn-ble.spec
srcs/CMakeLists.txt
srcs/exception.h
srcs/log/log.h
srcs/request_handler.h
srcs/tunnel.cpp [new file with mode: 0644]
srcs/tunnel.h [new file with mode: 0644]
srcs/websockets.cpp [new file with mode: 0644]
srcs/websockets.h [new file with mode: 0644]
tests/CMakeLists.txt
tests/tunnel/.gitignore [new file with mode: 0644]
tests/tunnel/auto_tests.cpp [new file with mode: 0644]
tests/tunnel/auto_tests.h [new file with mode: 0644]
tests/tunnel/cert.pem [new file with mode: 0644]
tests/tunnel/common_tests.cpp [new file with mode: 0644]
tests/tunnel/manual_tests.cpp [new file with mode: 0644]
tests/tunnel/manual_tests.h [new file with mode: 0644]
tests/tunnel/manual_tests.py [new file with mode: 0755]
tests/tunnel/manual_tests.sh.in [new file with mode: 0644]
tests/tunnel/privkey.pem [new file with mode: 0644]

index 86b9f1d3671468f07d2fd648d737ab5064a38fdd..28931c1f7f8036a22a2f55285ca6b38308c24219 100644 (file)
@@ -84,6 +84,7 @@ LINK_DIRECTORIES(${PROJECT_DEPS_LIBRARY_DIRS})
 SET(TARGET_WEBAUTHN_BLE "${PROJECT_NAME}")
 SET(TARGET_WEBAUTHN_BLE_UNIT_TESTS "${PROJECT_NAME}-unit-tests")
 SET(TARGET_WEBAUTHN_BLE_MANUAL_TESTS "${PROJECT_NAME}-manual-tests")
+SET(TARGET_WEBAUTHN_BLE_MANUAL_TUNNEL_TESTS "${PROJECT_NAME}-manual-tunnel-tests")
 
 ############################ Configure manifest files #########################
 CONFIGURE_FILE(packaging/${TARGET_WEBAUTHN_BLE}.manifest.in ${TARGET_WEBAUTHN_BLE}.manifest @ONLY)
index bb68ffbbe0ddbbc5c3a1c1336cbcbed61810e64e..c60259a3240898dd96592c69108fbd2eaa2f73aa 100644 (file)
@@ -94,3 +94,5 @@ rm -f %{WEBAUTHN_HYBRID_PLUGIN_SO_PATH}
 %manifest %{name}-manual-tests.manifest
 %license LICENSE
 %{_bindir}/%{name}-manual-tests
+%{_bindir}/%{name}-manual-tunnel-tests
+%{_bindir}/%{name}-manual-tunnel-tests.sh
index 9979dc72ede8bf2b67f8079ead5530ae8e3e3b4a..6c2f48422e5e1b74e81d99491141430f9f4aa412 100644 (file)
@@ -75,6 +75,8 @@ SET(WEBAUTHN_BLE_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/qr_transaction.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/state_assisted_transaction.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/qr_code_shower.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/tunnel.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/websockets.cpp
 )
 SET(WEBAUTHN_BLE_SOURCES ${WEBAUTHN_BLE_SOURCES} PARENT_SCOPE)
 
index b2dc611eab3a150309b38acd6da9a42e63a1f522..c61c1baa09be4c4ebdd3457f8a733f5aa8c5b9c6 100644 (file)
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "log/log.h"
+
 #include <stdexcept>
 #include <string>
 #include <type_traits>
@@ -51,3 +53,7 @@ typedef Exception<WAUTHN_ERROR_NO_SUCH_SERVICE> NoSuchService;
 typedef Exception<WAUTHN_ERROR_ACCESS_DENIED> AccessDenied;
 typedef Exception<WAUTHN_ERROR_MEMORY> MemoryError;
 typedef Exception<WAUTHN_ERROR_CANCELLED> Cancelled;
+
+#define THROW_UNKNOWN(message) LOGGED_THROW(Unknown, message)
+#define THROW_INVALID_STATE(message) LOGGED_THROW(InvalidState, message)
+#define THROW_CANCELLED() LOGGED_THROW(Cancelled, "Operation cancelled")
index 570158fdb77f2d8990a2b0f9291f56d731740cd4..87c73e76f8272f35ddc426e64eae7649b463d3de 100644 (file)
@@ -122,3 +122,9 @@ public:
         LogPedantic(__VA_ARGS__); \
     } catch (...) {               \
     }
+
+#define LOGGED_THROW(exceptionType, msg)              \
+    do {                                              \
+        TRY_LOG_ERROR("Throwing exception: " << msg); \
+        throw exceptionType(msg);                     \
+    } while (false)
index aa32b2b4be73d48f24297041a89d89853102205f..04657a3eec4d584b418e6df4e29c27b7647076e5 100644 (file)
@@ -108,6 +108,7 @@ public:
                 } catch (const abi::__forced_unwind &) {
                     throw;
                 } catch (...) {
+                    TRY_LOG_ERROR("Mutex lock failed");
                     result = WAUTHN_ERROR_UNKNOWN;
                 }
             });
@@ -130,6 +131,7 @@ public:
             } catch (const abi::__forced_unwind &) {
                 throw;
             } catch (...) {
+                TRY_LOG_ERROR("Response callback has thrown an exception");
             }
         }
     }
diff --git a/srcs/tunnel.cpp b/srcs/tunnel.cpp
new file mode 100644 (file)
index 0000000..b386371
--- /dev/null
@@ -0,0 +1,393 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  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 "exception.h"
+#include "tunnel.h"
+
+#include <arpa/inet.h>
+#include <cassert>
+#include <cstring>
+#include <unordered_map>
+#include <utility>
+
+namespace {
+
+#define PAIR(name)    \
+    {                 \
+        (name), #name \
+    }
+
+const std::unordered_map<enum lws_callback_reasons, std::string> REASON2STR = {
+    PAIR(LWS_CALLBACK_PROTOCOL_INIT),
+    PAIR(LWS_CALLBACK_PROTOCOL_DESTROY),
+    PAIR(LWS_CALLBACK_WSI_CREATE),
+    PAIR(LWS_CALLBACK_WSI_DESTROY),
+    PAIR(LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS),
+    PAIR(LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION),
+    PAIR(LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED),
+    PAIR(LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP),
+    PAIR(LWS_CALLBACK_CLOSED_CLIENT_HTTP),
+    PAIR(LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ),
+    PAIR(LWS_CALLBACK_RECEIVE_CLIENT_HTTP),
+    PAIR(LWS_CALLBACK_COMPLETED_CLIENT_HTTP),
+    PAIR(LWS_CALLBACK_CLIENT_HTTP_WRITEABLE),
+    PAIR(LWS_CALLBACK_CLIENT_HTTP_BIND_PROTOCOL),
+    PAIR(LWS_CALLBACK_CLOSED),
+    PAIR(LWS_CALLBACK_WS_PEER_INITIATED_CLOSE),
+    PAIR(LWS_CALLBACK_CLIENT_CONNECTION_ERROR),
+    PAIR(LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH),
+    PAIR(LWS_CALLBACK_CLIENT_ESTABLISHED),
+    PAIR(LWS_CALLBACK_CLIENT_CLOSED),
+    PAIR(LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER),
+    PAIR(LWS_CALLBACK_CLIENT_RECEIVE),
+    PAIR(LWS_CALLBACK_CLIENT_RECEIVE_PONG),
+    PAIR(LWS_CALLBACK_CLIENT_WRITEABLE),
+    PAIR(LWS_CALLBACK_CLIENT_CONFIRM_EXTENSION_SUPPORTED),
+    PAIR(LWS_CALLBACK_WS_EXT_DEFAULTS),
+    PAIR(LWS_CALLBACK_FILTER_NETWORK_CONNECTION),
+    PAIR(LWS_CALLBACK_GET_THREAD_ID),
+    PAIR(LWS_CALLBACK_EVENT_WAIT_CANCELLED),
+    PAIR(LWS_CALLBACK_CONNECTING),
+    PAIR(LWS_CALLBACK_VHOST_CERT_AGING),
+};
+
+const std::unordered_map<enum lws_close_status, std::string> CLOSE_CODE2STR = {
+    PAIR(LWS_CLOSE_STATUS_NORMAL),
+    PAIR(LWS_CLOSE_STATUS_GOINGAWAY),
+    PAIR(LWS_CLOSE_STATUS_PROTOCOL_ERR),
+    PAIR(LWS_CLOSE_STATUS_UNACCEPTABLE_OPCODE),
+    PAIR(LWS_CLOSE_STATUS_RESERVED),
+    PAIR(LWS_CLOSE_STATUS_NO_STATUS),
+    PAIR(LWS_CLOSE_STATUS_ABNORMAL_CLOSE),
+    PAIR(LWS_CLOSE_STATUS_INVALID_PAYLOAD),
+    PAIR(LWS_CLOSE_STATUS_POLICY_VIOLATION),
+    PAIR(LWS_CLOSE_STATUS_MESSAGE_TOO_LARGE),
+    PAIR(LWS_CLOSE_STATUS_EXTENSION_REQUIRED),
+    PAIR(LWS_CLOSE_STATUS_UNEXPECTED_CONDITION),
+    PAIR(LWS_CLOSE_STATUS_TLS_FAILURE),
+};
+
+#undef PAIR
+
+} // namespace
+
+Tunnel::Tunnel(std::shared_ptr<IWebsockets> ws)
+: m_ws(ws),
+  m_context(nullptr),
+  m_connection(nullptr),
+  m_state(State::DISCONNECTED),
+  m_cancelled(false)
+{
+    m_ws->SetListener(this);
+}
+
+Tunnel::~Tunnel()
+{
+    try {
+        Disconnect();
+        m_ws->SetListener(nullptr);
+    } catch (...) {
+        TRY_LOG_ERROR("Unexpected exception catched");
+    }
+}
+
+void Tunnel::Connect(const std::string &url)
+{
+    LogDebug("Connecting to " << url);
+
+    if (m_state != State::DISCONNECTED || m_context != nullptr)
+        THROW_INVALID_STATE("Already connected");
+
+    {
+        std::lock_guard<std::mutex> guard(m_mutex);
+        if (m_cancelled) {
+            m_cancelled = false;
+            THROW_CANCELLED();
+        }
+
+        m_context = m_ws->CreateContext();
+        if (!m_context)
+            THROW_UNKNOWN("Creating libwebsocket context failed");
+    }
+
+    m_connection = m_ws->ClientConnect(m_context, url);
+    if (!m_connection)
+        DisconnectOnError();
+
+    // Process the events generated during connecting until the connection is fully established.
+    while (!m_ws->Service(m_context) && m_state == State::DISCONNECTED) {
+    }
+
+    DisconnectOnError();
+}
+
+void Tunnel::WriteBinary(const std::vector<uint8_t> &msg)
+{
+    LogDebug("Writing " << msg.size() << "B");
+
+    if (m_state != State::CONNECTED)
+        THROW_INVALID_STATE("Not connected");
+
+    if (msg.empty())
+        THROW_UNKNOWN("Nothing to send");
+
+    m_out.resize(LWS_PRE + msg.size());
+    std::memcpy(m_out.data() + LWS_PRE, msg.data(), msg.size());
+    m_writtenBytesNum = 0;
+    m_state = State::WRITING;
+    if (!m_ws->CallbackOnWritable(m_connection)) {
+        LogError("CallbackOnWritable() failed");
+        m_state = State::FAILED;
+    } else {
+        while (!m_ws->Service(m_context) && m_state == State::WRITING) {
+        }
+    }
+
+    DisconnectOnError();
+}
+
+std::vector<uint8_t> Tunnel::ReadBinary()
+{
+    LogDebug("Reading");
+
+    if (m_state != State::CONNECTED)
+        THROW_INVALID_STATE("Not connected");
+
+    m_in.reserve(RX_BUFFER_SIZE);
+    m_in.clear();
+
+    m_state = State::READING;
+    while (!m_ws->Service(m_context) && m_state == State::READING) {
+    }
+
+    DisconnectOnError();
+    return std::move(m_in);
+}
+
+void Tunnel::Disconnect()
+{
+    if (!m_context) {
+        LogDebug("Not connected");
+        return;
+    }
+
+    std::unique_lock<std::mutex> lock(m_mutex);
+    DestroyContext(lock);
+}
+
+void Tunnel::Cancel()
+{
+    std::lock_guard<std::mutex> guard(m_mutex);
+
+    if (m_cancelled)
+        return;
+
+    m_cancelled = true;
+    if (m_context)
+        m_ws->CancelService(m_context);
+}
+
+bool Tunnel::HandleEvent(Lws *wsi, enum lws_callback_reasons reason, void *in, size_t len) noexcept
+{
+    try {
+        auto it = REASON2STR.find(reason);
+        if (it == REASON2STR.end())
+            LogDebug("Unknown reason: " << reason);
+        else
+            LogDebug(it->second);
+
+        switch (reason) {
+        case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: {
+            std::string description = "Unknown";
+            uint16_t code; // network order
+            if (in != nullptr && len >= sizeof(code)) {
+                memcpy(&code, in, sizeof(code));
+                code = ntohs(code);
+
+                auto it = CLOSE_CODE2STR.find(static_cast<enum lws_close_status>(code));
+                if (it != CLOSE_CODE2STR.end())
+                    description = it->second;
+
+                LogError("Peer initiated close. Code: " << code << " (" << description << ")");
+            } else {
+                LogError("Peer initiated close");
+            }
+
+            m_state = State::FAILED;
+            // returning false will echo the close and then close the connection
+            return false;
+        }
+
+        case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: {
+            std::string description;
+            if (in != nullptr)
+                description.assign(static_cast<char *>(in), static_cast<char *>(in) + len);
+
+            LogError("Connection error: " << description);
+
+            m_state = State::FAILED;
+            return true;
+        }
+
+        case LWS_CALLBACK_CLIENT_ESTABLISHED:
+            if (m_state != State::DISCONNECTED) {
+                LogError("Unexpected event");
+                m_state = State::FAILED;
+                return true;
+            }
+            m_state = State::CONNECTED;
+            break;
+
+        case LWS_CALLBACK_CLIENT_RECEIVE: {
+            bool binary = m_ws->FrameIsBinary(wsi);
+            bool first = m_ws->IsFirstFragment(wsi);
+            bool last = m_ws->IsFinalFragment(wsi);
+            uint8_t *data = reinterpret_cast<uint8_t *>(in);
+
+            LogDebug("Received " << len << "B, binary:" << binary << " first:" << first
+                                 << " last:" << last);
+
+            // I assume that LWS_CALLBACK_CLIENT_RECEIVE won't arrive during WriteBinary servicing
+            if (m_state != State::READING) {
+                LogDebug("Unexpected data");
+                break;
+            }
+
+            if (!binary) {
+                LogDebug("Not binary");
+                break;
+            }
+
+            if (first)
+                m_in.clear();
+
+            if (len > 0) {
+                if (!data) {
+                    LogError("Empty data");
+                    m_state = State::FAILED;
+                    break;
+                }
+                auto size = m_in.size();
+                m_in.resize(size + len);
+                std::memcpy(m_in.data() + size, data, len);
+            }
+
+            if (last)
+                m_state = State::CONNECTED;
+
+            break;
+        }
+
+        case LWS_CALLBACK_CLIENT_WRITEABLE: {
+            // These events happen from time to time. If we're not writing we just ignore them.
+            if (m_state != State::WRITING) {
+                LogDebug("Not writing");
+                break;
+            }
+
+            if (m_out.size() - m_writtenBytesNum <= LWS_PRE) {
+                m_state = State::FAILED;
+                LogError("Write buffer too short");
+                return true;
+            }
+
+            size_t toWrite = m_out.size() - m_writtenBytesNum - LWS_PRE;
+            int written =
+                m_ws->WriteBinary(wsi, m_out.data() + m_writtenBytesNum + LWS_PRE, toWrite);
+
+            if (written <= 0) {
+                m_state = State::FAILED;
+                LogError("lws_write() failed");
+                return true;
+            }
+
+            if (static_cast<size_t>(written) < toWrite) {
+                LogDebug("Truncated write " << written << " out of " << toWrite);
+                m_writtenBytesNum += written;
+
+                // Request another callback as soon as connection is writeable
+                if (!m_ws->CallbackOnWritable(m_connection)) {
+                    m_state = State::FAILED;
+                    LogError("CallbackOnWritable() failed");
+                    return true;
+                }
+            } else {
+                LogDebug("Written");
+                m_state = State::CONNECTED;
+            }
+            break;
+        }
+
+        case LWS_CALLBACK_EVENT_WAIT_CANCELLED: {
+            std::unique_lock<std::mutex> lock(m_mutex);
+            if (m_cancelled) {
+                LogDebug("Cancelled");
+                m_state = State::FAILED;
+                return true;
+            }
+            break;
+        }
+
+        default: break;
+        }
+        return false;
+    } catch (const std::exception &ex) {
+        TRY_LOG_ERROR("std::exception catched: " << ex.what());
+    } catch (...) {
+        TRY_LOG_ERROR("Unknown exception catched");
+    }
+    m_state = State::FAILED;
+    return true;
+}
+
+// must be called with m_mutex locked
+void Tunnel::DestroyContext(std::unique_lock<std::mutex> &lock)
+{
+    LogDebug("Destroying context");
+
+    assert(lock.owns_lock());
+    assert(m_context != nullptr);
+
+    auto tmp = m_context;
+    m_context = nullptr;
+    m_connection = nullptr;
+
+    // Possible cancellation has already been handled. Reset the flag.
+    m_cancelled = false;
+
+    lock.unlock();
+
+    // Let the destruction events be processed outside of the mutex to avoid a deadlock in
+    // HandleEvent()
+    m_ws->ContextDestroy(tmp);
+
+    // Websockets::ContextDestroy() may have affected the m_state. Set it now.
+    m_state = State::DISCONNECTED;
+
+    LogDebug("Destroying finished");
+}
+
+void Tunnel::DisconnectOnError()
+{
+    std::unique_lock<std::mutex> lock(m_mutex);
+    if (m_cancelled) {
+        DestroyContext(lock);
+        THROW_CANCELLED();
+    } else if (m_state != State::CONNECTED) {
+        DestroyContext(lock);
+        THROW_INVALID_STATE("Invalid tunnel state");
+    }
+}
diff --git a/srcs/tunnel.h b/srcs/tunnel.h
new file mode 100644 (file)
index 0000000..2133533
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  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
+ */
+
+#pragma once
+
+#include "websockets.h"
+
+#include <cstdint>
+#include <libwebsockets.h>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+class ITunnel {
+protected:
+    ITunnel() = default;
+
+public:
+    virtual ~ITunnel() = default;
+
+    virtual void Connect(const std::string &url) = 0;
+    virtual void WriteBinary(const std::vector<uint8_t> &msg) = 0;
+    virtual std::vector<uint8_t> ReadBinary() = 0;
+    virtual void Disconnect() = 0;
+    virtual void Cancel() = 0;
+};
+
+class Tunnel : public ITunnel, public IWebsocketsListener {
+public:
+    explicit Tunnel(std::shared_ptr<IWebsockets> ws = std::make_shared<Websockets>());
+    ~Tunnel();
+
+    Tunnel(const Tunnel &) = delete;
+    Tunnel &operator=(const Tunnel &) = delete;
+
+    void Connect(const std::string &url) override;
+    void WriteBinary(const std::vector<uint8_t> &msg) override;
+    std::vector<uint8_t> ReadBinary() override;
+    void Disconnect() override;
+
+    /*
+     *  Cancels the operation currently being processed and if there's none, the first operation
+     *  processed after call to this method.
+     */
+    void Cancel() override;
+
+    enum class State {
+        DISCONNECTED,
+        FAILED,
+        CONNECTED,
+        WRITING,
+        READING,
+    };
+
+    bool
+    HandleEvent(Lws *wsi, enum lws_callback_reasons reason, void *in, size_t len) noexcept override;
+
+protected:
+    void DestroyContext(std::unique_lock<std::mutex> &lock);
+    void DisconnectOnError();
+
+    std::shared_ptr<IWebsockets> m_ws;
+    LwsContext *m_context;
+    Lws *m_connection;
+    std::vector<uint8_t> m_in, m_out;
+    size_t m_writtenBytesNum;
+    State m_state;
+    bool m_cancelled;
+
+    /*
+     * Protects m_cancelled and m_context.
+     *
+     * Another thread can only read m_context and write m_cancelled in Cancel() under the mutex.
+     *
+     * Main thread reads and writes m_cancelled, and changes m_context under the mutex. Reading
+     * m_context in the main thread is safe to do without the mutex, since the other thread does not
+     * modify it.
+     */
+    std::mutex m_mutex;
+};
+
diff --git a/srcs/websockets.cpp b/srcs/websockets.cpp
new file mode 100644 (file)
index 0000000..f7752bb
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  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 "log/log.h"
+#include "websockets.h"
+
+#include <vector>
+
+namespace {
+
+constexpr int WSS_PORT = 443;
+
+struct lws *Lws2Native(Lws *wsi) noexcept { return reinterpret_cast<struct lws *>(wsi); }
+
+struct lws_context *Context2Native(LwsContext *context) noexcept
+{
+    return reinterpret_cast<struct lws_context *>(context);
+}
+
+Lws *Native2Lws(struct lws *wsi) noexcept { return reinterpret_cast<Lws *>(wsi); }
+
+LwsContext *Native2Context(struct lws_context *context) noexcept
+{
+    return reinterpret_cast<LwsContext *>(context);
+}
+
+void WsLog(int level, const char *line) noexcept
+{
+    switch (level) {
+    case LLL_ERR: TRY_LOG_ERROR(line); break;
+    case LLL_WARN: TRY_LOG_WARNING(line); break;
+    default: TRY_LOG_DEBUG(line); break;
+    }
+}
+
+bool RedirectWebsocketsLogs() noexcept
+{
+    lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE, WsLog);
+    return true;
+}
+
+} // namespace
+
+Websockets::Websockets() noexcept
+: m_protocols{
+      {"fido.cable", FidoCallback, 0, RX_BUFFER_SIZE, 0, this, 0},
+      LWS_PROTOCOL_LIST_TERM
+}
+{
+    [[maybe_unused]] static bool redirected = RedirectWebsocketsLogs();
+}
+
+LwsContext *Websockets::CreateContext() noexcept
+{
+    struct lws_context_creation_info contextInfo;
+
+    memset(&contextInfo, 0, sizeof contextInfo);
+
+    contextInfo.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
+    contextInfo.port = CONTEXT_PORT_NO_LISTEN; // we are not a server
+    contextInfo.protocols = m_protocols;
+    contextInfo.gid = (gid_t)-1;
+    contextInfo.uid = (uid_t)-1;
+
+    return Native2Context(lws_create_context(&contextInfo));
+}
+
+Lws *Websockets::DoClientConnect(LwsContext *context, const std::string &url, int sslFlags) noexcept
+{
+    struct lws_client_connect_info cInfo;
+    const char *protocol, *path1;
+    char path[128];
+    std::vector<char> urlMutable(url.c_str(), url.c_str() + url.size() + 1);
+
+    memset(&cInfo, 0, sizeof cInfo);
+
+    cInfo.port = WSS_PORT;
+    if (lws_parse_uri(urlMutable.data(), &protocol, &cInfo.address, &cInfo.port, &path1)) {
+        TRY_LOG_ERROR("lws_parse_uri() failed for " << url);
+        return nullptr;
+    }
+
+    path[0] = '/';
+    unsigned n = 0;
+    if (path1[0] == '/')
+        n = 1;
+    lws_strncpy(&path[1], &path1[n], sizeof(path) - n);
+
+    if (strcmp(protocol, "wss") != 0) {
+        TRY_LOG_ERROR("Unexpected protocol " << protocol);
+        return nullptr;
+    }
+
+    cInfo.context = Context2Native(context);
+    cInfo.ssl_connection = sslFlags;
+    cInfo.host = cInfo.address;
+    cInfo.origin = cInfo.address;
+    cInfo.ietf_version_or_minus_one = -1;
+    cInfo.protocol = m_protocols[0].name;
+    cInfo.pwsi = nullptr;
+    cInfo.path = path;
+
+    return Native2Lws(lws_client_connect_via_info(&cInfo));
+}
+
+Lws *Websockets::ClientConnect(LwsContext *context, const std::string &url) noexcept
+{
+    return DoClientConnect(context, url, LCCSCF_USE_SSL);
+}
+
+bool Websockets::Service(LwsContext *context) noexcept
+{
+    return lws_service(Context2Native(context), 0) != 0;
+}
+
+bool Websockets::CallbackOnWritable(Lws *wsi) noexcept
+{
+    return lws_callback_on_writable(Lws2Native(wsi)) != 0;
+}
+
+void Websockets::CancelService(LwsContext *context) noexcept
+{
+    lws_cancel_service(Context2Native(context));
+}
+
+bool Websockets::FrameIsBinary(Lws *wsi) const noexcept
+{
+    return lws_frame_is_binary(Lws2Native(wsi)) != 0;
+}
+
+bool Websockets::IsFirstFragment(Lws *wsi) const noexcept
+{
+    return lws_is_first_fragment(Lws2Native(wsi)) != 0;
+}
+
+bool Websockets::IsFinalFragment(Lws *wsi) const noexcept
+{
+    return lws_is_final_fragment(Lws2Native(wsi)) != 0;
+}
+
+int Websockets::WriteBinary(Lws *wsi, unsigned char *buf, size_t len) noexcept
+{
+    return lws_write(Lws2Native(wsi), buf, len, LWS_WRITE_BINARY);
+}
+
+void Websockets::ContextDestroy(LwsContext *context) noexcept
+{
+    lws_context_destroy(Context2Native(context));
+}
+
+int Websockets::FidoCallback(struct lws *wsi,
+                             enum lws_callback_reasons reason,
+                             void * /*user*/,
+                             void *in,
+                             size_t len) noexcept
+{
+    if (wsi == nullptr) {
+        TRY_LOG_ERROR("Wsi is NULL");
+        return 1;
+    }
+
+    auto proto = lws_get_protocol(wsi);
+    if (proto == nullptr) {
+        // Normally it happes 2 times during the connection establishment.
+        TRY_LOG_DEBUG("Protocol is NULL");
+        return 0;
+    }
+
+    auto *ws = static_cast<Websockets *>(proto->user);
+    if (ws == nullptr) {
+        TRY_LOG_ERROR("User data is NULL");
+        return 1;
+    }
+
+    if (ws->m_listener == nullptr) {
+        TRY_LOG_ERROR("There's no listener");
+        return 1;
+    }
+
+    return ws->m_listener->HandleEvent(Native2Lws(wsi), reason, in, len) ? 1 : 0;
+}
diff --git a/srcs/websockets.h b/srcs/websockets.h
new file mode 100644 (file)
index 0000000..ca1a03b
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  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
+ */
+
+#pragma once
+
+#include <cstddef>
+#include <libwebsockets.h>
+#include <string>
+
+struct LwsContext;
+struct Lws;
+
+class IWebsocketsListener {
+protected:
+    IWebsocketsListener() = default;
+
+public:
+    virtual ~IWebsocketsListener() = default;
+
+    /*
+     * @brief  Callback to be triggered when a websocket event occurs
+     *
+     * @param[in] wsi  The connection affected by the event
+     * @param[in] reason  The type of the event
+     * @param[in] in  Optional event data (format depends on the event type)
+     * @param[in] len  Length of the @in buffer
+     *
+     * @return true if the connection should be closed (e.g. in case of error), false otherwise
+     */
+    virtual bool
+    HandleEvent(Lws *wsi, enum lws_callback_reasons reason, void *in, size_t len) noexcept = 0;
+};
+
+constexpr int RX_BUFFER_SIZE = 4096;
+
+class IWebsockets {
+protected:
+    IWebsockets() = default;
+
+public:
+    virtual ~IWebsockets() = default;
+
+    /*
+     * @brief Initializes the context.
+     *
+     * @return nullptr on error
+     */
+    virtual LwsContext *CreateContext() noexcept = 0;
+
+    /*
+     * @brief Makes a connection to a websocket server.
+     *
+     * @param[in] context  Context created with CreateContext() used to handle websocket events.
+     * @param[in] url  Server's url.
+     *
+     * @return nullptr on error, a connection otherwise
+     */
+    virtual Lws *ClientConnect(LwsContext *context, const std::string &url) noexcept = 0;
+
+    /*
+     * @brief Processes websocket events.
+     *
+     * @param[in] context  Context created with CreateContext() used to handle websocket events of
+     *                     all connections. Here limited to a single connection
+     *
+     * @return true if event processing should be stopped (e.g. because of an error), false
+     *         otherwise
+     */
+    virtual bool Service(LwsContext *context) noexcept = 0;
+
+    /*
+     * @brief Asks to trigger LWS_CALLBACK_CLIENT_WRITEABLE event as soon as the connection is
+     *        writeable.
+     *
+     * @param[in] wsi  A connection to be written to created with ClientConnect()
+     *
+     * @return true if it succeeds, false otherwise
+     */
+    virtual bool CallbackOnWritable(Lws *wsi) noexcept = 0;
+
+    /*
+     * @brief Cancels waiting for new events and schedules LWS_CALLBACK_EVENT_WAIT_CANCELLED event
+     *        as soon as possible. May be called from another thread.
+     *
+     * @param[in] context  Context created with CreateContext() to which the cancelling should be
+     *                     applied
+     */
+    virtual void CancelService(LwsContext *context) noexcept = 0;
+
+    /*
+     * @brief  Checks if incoming frame is of binary type
+     *
+     * @param[in] wsi  The connection which received the frame in question
+     *
+     * @return true if it's a binary frame, false otherwise
+     */
+    virtual bool FrameIsBinary(Lws *wsi) const noexcept = 0;
+
+    /*
+     * @brief  Checks if it's the first part of incoming frame
+     *
+     * @param[in] wsi  The connection which received the frame in question
+     *
+     * @return true if it's the first part, false otherwise
+     */
+    virtual bool IsFirstFragment(Lws *wsi) const noexcept = 0;
+
+    /*
+     * @brief  Checks if it's the last part of incoming frame
+     *
+     * @param[in] wsi  The connection which received the frame in question
+     *
+     * @return true if it's the last part, false otherwise
+     */
+    virtual bool IsFinalFragment(Lws *wsi) const noexcept = 0;
+
+    /*
+     * @brief  Send binary frame using given connection. The connection must be writeable.
+     *
+     * @param[in] wsi  The connection to be used for sending
+     * @param[in] buf  Data to be sent
+     * @param[in] len  Size of the @a buf
+     *
+     * @return  Amount of bytes written
+     */
+    virtual int WriteBinary(Lws *wsi, unsigned char *buf, size_t len) noexcept = 0;
+
+    /*
+     * @brief  Destroys given context and releases resources and closes connection if necessary
+     *
+     * @param[in] context  The context to be destroyed
+     */
+    virtual void ContextDestroy(LwsContext *context) noexcept = 0;
+
+    /*
+     * @brief  Sets the listener of websocket events
+     *
+     * @param[in] listener  The listener to set
+     */
+    virtual void SetListener(IWebsocketsListener *listener) noexcept = 0;
+};
+
+class Websockets : public IWebsockets {
+public:
+    Websockets() noexcept;
+    ~Websockets() = default;
+
+    LwsContext *CreateContext() noexcept override;
+    Lws *ClientConnect(LwsContext *context, const std::string &url) noexcept override;
+    bool Service(LwsContext *context) noexcept override;
+    bool CallbackOnWritable(Lws *wsi) noexcept override;
+    void CancelService(LwsContext *context) noexcept override;
+    bool FrameIsBinary(Lws *wsi) const noexcept override;
+    bool IsFirstFragment(Lws *wsi) const noexcept override;
+    bool IsFinalFragment(Lws *wsi) const noexcept override;
+    int WriteBinary(Lws *wsi, unsigned char *buf, size_t len) noexcept override;
+    void ContextDestroy(LwsContext *context) noexcept override;
+
+    void SetListener(IWebsocketsListener *listener) noexcept override { m_listener = listener; }
+
+protected:
+    Lws *DoClientConnect(LwsContext *context, const std::string &url, int sslFlags) noexcept;
+
+    IWebsocketsListener *m_listener = nullptr;
+
+private:
+    // return 1 if the connection should be closed (e.g. in case of error), 0 otherwise
+    static int FidoCallback(struct lws *wsi,
+                            enum lws_callback_reasons reason,
+                            void *user,
+                            void *in,
+                            size_t len) noexcept;
+
+    const struct lws_protocols m_protocols[2];
+};
index 65c94c42175ffaabe59e7cf84fbbc90b3a5a418a..ccb094725e722c06e375ce8e08f43bab63f493af 100644 (file)
@@ -26,6 +26,7 @@ INCLUDE_DIRECTORIES(${TINYCBOR_DIR}/src/)
 
 LINK_DIRECTORIES(${TESTS_DEPS_LIBRARY_DIRS})
 
+# unit tests
 SET(UNIT_TESTS_SOURCES
     ${WEBAUTHN_BLE_SOURCES}
     ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
@@ -47,6 +48,8 @@ SET(UNIT_TESTS_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/crypto/ec_key_unittest.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/crypto/ecdh_unittest.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/crypto/noise/noise_unittest.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/tunnel/auto_tests.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/tunnel/common_tests.cpp
 )
 
 ADD_EXECUTABLE(${TARGET_WEBAUTHN_BLE_UNIT_TESTS} ${UNIT_TESTS_SOURCES})
@@ -62,6 +65,7 @@ TARGET_LINK_LIBRARIES(${TARGET_WEBAUTHN_BLE_UNIT_TESTS}
 
 INSTALL(TARGETS ${TARGET_WEBAUTHN_BLE_UNIT_TESTS} DESTINATION ${BIN_DIR})
 
+# manual full-scenario test
 SET(MANUAL_TESTS_SOURCES
     ${WEBAUTHN_BLE_SOURCES}
     ${CMAKE_CURRENT_SOURCE_DIR}/man_tests.cpp
@@ -80,3 +84,31 @@ TARGET_LINK_LIBRARIES(${TARGET_WEBAUTHN_BLE_MANUAL_TESTS}
 )
 
 INSTALL(TARGETS ${TARGET_WEBAUTHN_BLE_MANUAL_TESTS} DESTINATION ${BIN_DIR})
+
+# manual tunnel tests
+SET(MANUAL_TUNNEL_TESTS_SOURCES
+    ${PROJECT_SOURCE_DIR}/srcs/log/log.cpp
+    ${PROJECT_SOURCE_DIR}/srcs/log/dlog_log_provider.cpp
+    ${PROJECT_SOURCE_DIR}/srcs/log/abstract_log_provider.cpp
+    ${PROJECT_SOURCE_DIR}/srcs/tunnel.cpp
+    ${PROJECT_SOURCE_DIR}/srcs/websockets.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/tunnel/manual_tests.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/tunnel/common_tests.cpp
+)
+
+ADD_EXECUTABLE(${TARGET_WEBAUTHN_BLE_MANUAL_TUNNEL_TESTS} ${MANUAL_TUNNEL_TESTS_SOURCES})
+
+TARGET_LINK_LIBRARIES(
+    ${TARGET_WEBAUTHN_BLE_MANUAL_TUNNEL_TESTS}
+    ${CMAKE_THREAD_LIBS_INIT}
+    ${PROJECT_DEPS_LIBRARIES}
+    ${TESTS_DEPS_LIBRARIES}
+)
+
+TARGET_COMPILE_DEFINITIONS(${TARGET_WEBAUTHN_BLE_MANUAL_TUNNEL_TESTS} PRIVATE MANUAL_TESTS)
+
+INSTALL(TARGETS ${TARGET_WEBAUTHN_BLE_MANUAL_TUNNEL_TESTS} DESTINATION ${BIN_DIR})
+
+CONFIGURE_FILE(tunnel/manual_tests.sh.in ${TARGET_WEBAUTHN_BLE_MANUAL_TUNNEL_TESTS}.sh @ONLY)
+
+INSTALL(PROGRAMS "${TARGET_WEBAUTHN_BLE_MANUAL_TUNNEL_TESTS}.sh" DESTINATION ${BIN_DIR})
diff --git a/tests/tunnel/.gitignore b/tests/tunnel/.gitignore
new file mode 100644 (file)
index 0000000..f21161f
--- /dev/null
@@ -0,0 +1 @@
+dummy_server.log
diff --git a/tests/tunnel/auto_tests.cpp b/tests/tunnel/auto_tests.cpp
new file mode 100644 (file)
index 0000000..1e6a2fa
--- /dev/null
@@ -0,0 +1,606 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  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 "../get_random.h"
+#include "../test_cancel_from_the_other_thread.h"
+#include "auto_tests.h"
+#include "exception.h"
+#include "tunnel.h"
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <deque>
+#include <iostream>
+#include <memory>
+#include <thread>
+#include <unistd.h>
+
+namespace {
+struct Event {
+    explicit Event(enum lws_callback_reasons reason, std::vector<uint8_t> data)
+    : m_reason(reason), m_data(std::move(data))
+    {
+    }
+    enum lws_callback_reasons m_reason;
+    std::vector<uint8_t> m_data;
+};
+} // namespace
+
+const std::string &TestUrl()
+{
+    static const std::string TEST_URL = "wss://whatever.io";
+    return TEST_URL;
+}
+
+struct MockedContext {
+    MockedLws *m_connection = nullptr;
+    bool m_cancelled = false;
+    std::mutex m_mutex;
+};
+
+struct MockedLws {
+    MockedLws(MockedContext *ctx, const std::string &url) : m_mockedContext(ctx), m_url(url) {}
+
+    MockedContext *m_mockedContext = nullptr;
+    std::string m_url;
+    std::deque<Event> m_queue;
+    std::mutex m_mutex;
+
+    void PushEvent(enum lws_callback_reasons reason,
+                   std::vector<uint8_t> data = std::vector<uint8_t>())
+    {
+        std::lock_guard<std::mutex> lock(m_mutex);
+        m_queue.emplace_back(reason, std::move(data));
+    }
+};
+
+namespace {
+MockedLws *Lws2Mocked(Lws *lws) noexcept { return reinterpret_cast<MockedLws *>(lws); }
+
+struct MockedContext *Context2Mocked(LwsContext *lwsContext) noexcept
+{
+    return reinterpret_cast<MockedContext *>(lwsContext);
+}
+
+Lws *Mocked2Lws(MockedLws *mockedLws) noexcept { return reinterpret_cast<Lws *>(mockedLws); }
+
+LwsContext *Mocked2Context(MockedContext *mockedContext) noexcept
+{
+    return reinterpret_cast<LwsContext *>(mockedContext);
+}
+
+template <typename F>
+class HookedSockets : public MockedSockets {
+public:
+    explicit HookedSockets(F &hook) : m_hook(hook) {}
+
+    void RunBefore(unsigned eventNo)
+    {
+        m_runBefore = eventNo;
+        m_eventNo = 0;
+    }
+
+    void ProcessEvent(MockedLws *mockedLws,
+                      enum lws_callback_reasons reason,
+                      std::vector<uint8_t> data = {}) override
+    {
+        if (m_eventNo == m_runBefore)
+            m_hook();
+
+        MockedSockets::ProcessEvent(mockedLws, reason, data);
+        m_eventNo++;
+    }
+
+private:
+    unsigned m_eventNo = 0;
+    unsigned m_runBefore = 0;
+    F &m_hook;
+};
+
+class DelayedSockets : public MockedSockets {
+    void RandomDelay() const noexcept
+    {
+        try {
+            std::this_thread::sleep_for(std::chrono::nanoseconds{get_random(0, 100'000)});
+        } catch (...) {
+        }
+    }
+
+public:
+    LwsContext *CreateContext() noexcept override
+    {
+        RandomDelay();
+        auto res = MockedSockets::CreateContext();
+        RandomDelay();
+        return res;
+    }
+
+    Lws *ClientConnect(LwsContext *lwsContext, const std::string &url) noexcept override
+    {
+        RandomDelay();
+        auto res = MockedSockets::ClientConnect(lwsContext, url);
+        RandomDelay();
+        return res;
+    }
+
+    bool Service(LwsContext *lwsContext) noexcept override
+    {
+        RandomDelay();
+        auto res = MockedSockets::Service(lwsContext);
+        RandomDelay();
+        return res;
+    }
+
+    bool CallbackOnWritable(Lws *lws) noexcept override
+    {
+        RandomDelay();
+        auto res = MockedSockets::CallbackOnWritable(lws);
+        RandomDelay();
+        return res;
+    }
+
+    bool FrameIsBinary(Lws *lws) const noexcept override
+    {
+        RandomDelay();
+        auto res = MockedSockets::FrameIsBinary(lws);
+        RandomDelay();
+        return res;
+    }
+
+    bool IsFirstFragment(Lws *lws) const noexcept override
+    {
+        RandomDelay();
+        auto res = MockedSockets::IsFirstFragment(lws);
+        RandomDelay();
+        return res;
+    }
+
+    bool IsFinalFragment(Lws *lws) const noexcept override
+    {
+        RandomDelay();
+        auto res = MockedSockets::IsFinalFragment(lws);
+        RandomDelay();
+        return res;
+    }
+
+    int WriteBinary(Lws *lws, unsigned char *buf, size_t len) noexcept override
+    {
+        RandomDelay();
+        auto res = MockedSockets::WriteBinary(lws, buf, len);
+        RandomDelay();
+        return res;
+    }
+
+    void ContextDestroy(LwsContext *lwsContext) noexcept override
+    {
+        RandomDelay();
+        MockedSockets::ContextDestroy(lwsContext);
+        RandomDelay();
+    }
+
+    void SetListener(IWebsocketsListener *listener) noexcept override
+    {
+        RandomDelay();
+        MockedSockets::SetListener(listener);
+        RandomDelay();
+    }
+};
+
+} // namespace
+
+void MockedSockets::ProcessEvent(MockedLws *mockedLws,
+                                 enum lws_callback_reasons reason,
+                                 std::vector<uint8_t> data)
+{
+    auto lws = Mocked2Lws(mockedLws);
+    m_listener->HandleEvent(lws, reason, data.empty() ? nullptr : data.data(), data.size());
+}
+
+LwsContext *MockedSockets::CreateContext() noexcept
+{
+    return Mocked2Context(new (std::nothrow) MockedContext);
+}
+
+Lws *MockedSockets::ClientConnect(LwsContext *lwsContext, const std::string &url) noexcept
+{
+    try {
+        auto mockedContext = Context2Mocked(lwsContext);
+        {
+            std::lock_guard<std::mutex> guard(mockedContext->m_mutex);
+            if (mockedContext->m_cancelled)
+                return nullptr;
+            mockedContext->m_connection = new MockedLws(mockedContext, url);
+        }
+        auto mockedLws = mockedContext->m_connection;
+
+        // process events immediately
+        ProcessEvent(mockedLws, LWS_CALLBACK_PROTOCOL_INIT);
+        ProcessEvent(mockedLws, LWS_CALLBACK_CLIENT_HTTP_BIND_PROTOCOL);
+        ProcessEvent(mockedLws, LWS_CALLBACK_CONNECTING);
+        ProcessEvent(mockedLws, LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED);
+
+        // schedule events
+        mockedLws->PushEvent(LWS_CALLBACK_WSI_CREATE);
+        mockedLws->PushEvent(LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION);
+        mockedLws->PushEvent(LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER);
+        mockedLws->PushEvent(LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP);
+        mockedLws->PushEvent(LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH);
+        mockedLws->PushEvent(LWS_CALLBACK_CLIENT_ESTABLISHED);
+
+        return Mocked2Lws(mockedLws);
+    } catch (...) {
+        return nullptr;
+    }
+}
+
+bool MockedSockets::Service(LwsContext *lwsContext) noexcept
+{
+    try {
+        auto mockedContext = Context2Mocked(lwsContext);
+
+        auto mockedLws = mockedContext->m_connection;
+        auto &queue = mockedLws->m_queue;
+
+        {
+            std::unique_lock<std::mutex> lock(mockedLws->m_mutex);
+            // this is to skip new events added during processing of current events
+            auto size = queue.size();
+            for (size_t i = 0; i < size; i++) {
+
+                auto event = std::move(queue.front());
+                queue.pop_front();
+
+                // process outside of mutex to allow pushing
+                lock.unlock();
+                ProcessEvent(mockedLws, event.m_reason, event.m_data);
+                lock.lock();
+            }
+        }
+        // push it from time to time to allow writing
+        mockedLws->PushEvent(LWS_CALLBACK_CLIENT_WRITEABLE);
+        return false;
+    } catch (...) {
+        return true;
+    }
+}
+
+bool MockedSockets::CallbackOnWritable(Lws *lws) noexcept
+{
+    auto mockedLws = Lws2Mocked(lws);
+    try {
+        mockedLws->PushEvent(LWS_CALLBACK_CLIENT_WRITEABLE);
+        return true;
+    } catch (...) {
+        return false;
+    }
+}
+
+// only this method can be called from a different thread
+void MockedSockets::CancelService(LwsContext *lwsContext) noexcept
+{
+    auto mockedContext = Context2Mocked(lwsContext);
+    try {
+        std::lock_guard<std::mutex> guard(mockedContext->m_mutex);
+        mockedContext->m_cancelled = true;
+        if (mockedContext->m_connection)
+            mockedContext->m_connection->PushEvent(LWS_CALLBACK_EVENT_WAIT_CANCELLED);
+    } catch (...) {
+        FAIL() << "Unexpected exception";
+    }
+}
+
+int MockedSockets::WriteBinary(Lws *lws, unsigned char *buf, size_t len) noexcept
+{
+    assert(buf && len > 0);
+
+    auto mockedLws = Lws2Mocked(lws);
+    try {
+        std::vector<uint8_t> buffer(buf, buf + len);
+
+        // perform "echo" response
+        mockedLws->PushEvent(LWS_CALLBACK_CLIENT_RECEIVE, std::move(buffer));
+        return len;
+    } catch (...) {
+        return 0;
+    }
+}
+
+void MockedSockets::ContextDestroy(LwsContext *lwsContext) noexcept
+{
+    try {
+        auto mockedContext = Context2Mocked(lwsContext);
+        auto mockedLws = mockedContext->m_connection;
+
+        ProcessEvent(mockedLws, LWS_CALLBACK_CLIENT_CLOSED);
+        ProcessEvent(mockedLws, LWS_CALLBACK_WSI_DESTROY);
+        ProcessEvent(mockedLws, LWS_CALLBACK_PROTOCOL_DESTROY);
+
+        std::lock_guard<std::mutex> guard(mockedContext->m_mutex);
+        delete mockedContext->m_connection;
+        mockedContext->m_connection = nullptr;
+
+        delete mockedContext;
+    } catch (...) {
+        FAIL() << "Unexpected exception";
+    }
+}
+
+/*
+ * Tests below are executed using fully mocked websockets implementation only (MockedSockets)
+ */
+TEST(TunnelMockedTests, Cancelling)
+{
+    const std::string test = "sdfjisdjkfhdfjkghsdfkghdgkjhd";
+    std::vector<uint8_t> in, out;
+    out.assign(test.c_str(), test.c_str() + test.size());
+
+    std::unique_ptr<Tunnel> tunnel;
+    auto cancel = [&] { tunnel->Cancel(); };
+    auto sockets = std::make_shared<HookedSockets<decltype(cancel)>>(cancel);
+    tunnel.reset(new Tunnel(sockets));
+
+    for (unsigned eventNo = 0; eventNo < 15; eventNo++) {
+        sockets->RunBefore(eventNo);
+
+        try {
+            tunnel->Connect(TestUrl());
+            EXPECT_GE(eventNo,
+                      10); // we shouldn't get this far if cancel was called before 10th event
+
+            tunnel->WriteBinary(out);
+            EXPECT_GE(eventNo,
+                      12); // we shouldn't get this far if cancel was called before 12th event
+
+            in = tunnel->ReadBinary();
+            EXPECT_EQ(in, out);
+            EXPECT_GE(eventNo,
+                      14); // we shouldn't get this far if cancel was called before 14th event
+
+            EXPECT_NO_THROW(tunnel->Disconnect());
+        } catch (const Cancelled &) {
+        } catch (...) {
+            FAIL() << "Unexpected exception";
+        }
+    }
+}
+
+TEST(TunnelMockedTests, InjectedEvents)
+{
+    // Events to be injected
+    static constexpr std::array REASONS{
+        LWS_CALLBACK_USER, // skips injection
+        LWS_CALLBACK_WS_PEER_INITIATED_CLOSE,
+        LWS_CALLBACK_CLIENT_CONNECTION_ERROR,
+        LWS_CALLBACK_CLIENT_ESTABLISHED,
+        LWS_CALLBACK_CLIENT_RECEIVE,
+        LWS_CALLBACK_CLIENT_WRITEABLE,
+        LWS_CALLBACK_EVENT_WAIT_CANCELLED,
+    };
+
+    struct EventInfo {
+        int id;
+        enum lws_callback_reasons reason;
+    };
+
+    static constexpr std::array EXPECTED_ORDER{
+  // called by Tunnel::Connect->MockedSockets::ClientConnect()
+        EventInfo{-1, LWS_CALLBACK_PROTOCOL_INIT                           },
+        EventInfo{-1, LWS_CALLBACK_CLIENT_HTTP_BIND_PROTOCOL               },
+        EventInfo{-1, LWS_CALLBACK_CONNECTING                              },
+        EventInfo{-1, LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED          },
+ // pushed by Tunnel::Connect->MockedSockets::ClientConnect()
+        EventInfo{-1, LWS_CALLBACK_WSI_CREATE                              },
+        EventInfo{-1, LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION},
+        EventInfo{-1, LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER          },
+        EventInfo{-1, LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP                 },
+        EventInfo{-1, LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH             },
+        EventInfo{0,  LWS_CALLBACK_CLIENT_ESTABLISHED                      },
+ // pushed by Tunnel::Connect()->MockedSockets::Service()
+        EventInfo{1,  LWS_CALLBACK_CLIENT_WRITEABLE                        },
+ // pushed by Tunnel::WriteBinary()->MockedSockets::CallbackOnWritable()
+        EventInfo{2,  LWS_CALLBACK_CLIENT_WRITEABLE                        },
+ // pushed by Tunnel::WriteBinary()->MockedSockets::WriteBinary()
+        EventInfo{3,  LWS_CALLBACK_CLIENT_RECEIVE                          },
+ // pushed by Tunnel::WriteBinary()->MockedSockets::Service()
+        EventInfo{4,  LWS_CALLBACK_CLIENT_WRITEABLE                        },
+ // called by Tunnel::Disconnect()->MockedSockets::ContextDestroy()
+        EventInfo{5,  LWS_CALLBACK_CLIENT_CLOSED                           },
+        EventInfo{-1, LWS_CALLBACK_WSI_DESTROY                             },
+        EventInfo{-1, LWS_CALLBACK_PROTOCOL_DESTROY                        }
+    };
+
+    // Places of event injection (before event with given EventInfo::id)
+    constexpr int EVENT_ID_CNT = [&] {
+        int maxId = -1;
+        for (const auto &eventInfo : EXPECTED_ORDER) {
+            if (eventInfo.id > maxId)
+                maxId = eventInfo.id;
+        }
+        return maxId + 1;
+    }();
+
+    // Max number of injections
+    constexpr size_t MAX_INJECTIONS = 3;
+
+    using InjectionsMap = std::vector<EventInfo>;
+    auto iterateAllInjections = [&](auto &&callback) {
+        InjectionsMap injectionsMap;
+        injectionsMap.reserve(MAX_INJECTIONS);
+
+        // Places where to inject new events in non-decreasing order. -1 means no injection.
+        // Starts on [-1, ..., -1], ends on [EVENT_ID_CNT - 1, ..., EVENT_ID_CNT - 1]
+        std::array<int, MAX_INJECTIONS> places;
+        places.fill(-1);
+        // For each place not equal to -1 we will iterate all reasons indices giving at most
+        // pow(REASONS.size(), MAX_INJECTIONS) combinations
+        std::vector<size_t> reasonsIndices;
+        std::vector<size_t> lastReasonsIndices;
+        assert(EVENT_ID_CNT > 0 && "needed for the first iteration to be valid");
+        for (;;) {
+            // Skip places having value -1
+            size_t startPos = 0;
+            while (startPos < places.size() && places[startPos] == -1)
+                ++startPos;
+            // For current places iterate all possible reason combinations to inject
+            reasonsIndices.assign(MAX_INJECTIONS - startPos, 0);
+            lastReasonsIndices.assign(MAX_INJECTIONS - startPos, REASONS.size() - 1);
+            injectionsMap.resize(MAX_INJECTIONS - startPos, EventInfo{-1, LWS_CALLBACK_USER});
+            for (;;) {
+                // Generate injectionsMap
+                for (size_t i = startPos; i < places.size(); ++i)
+                    injectionsMap[i - startPos] =
+                        EventInfo{places[i], REASONS[reasonsIndices[i - startPos]]};
+                // Run callback
+                callback(static_cast<const decltype(injectionsMap) &>(injectionsMap));
+                // Increment reasonsIndices
+                if (reasonsIndices == lastReasonsIndices)
+                    break;
+                size_t i = reasonsIndices.size() - 1;
+                ++reasonsIndices[i];
+                while (reasonsIndices[i] == REASONS.size()) {
+                    reasonsIndices[i] = 0;
+                    ++reasonsIndices[i - 1];
+                    --i;
+                }
+            }
+
+            // Increment places
+            if (places.front() == EVENT_ID_CNT - 1)
+                break; // Nothing more to iterate
+
+            size_t i = places.size() - 1;
+            ++places[i];
+            while (places[i] == EVENT_ID_CNT) {
+                ++places[i - 1];
+                --i;
+            }
+            // Fix later places that are now equal to EVENT_ID_CNT
+            int minAvailablePlace = places[i];
+            while (++i < places.size())
+                places[i] = minAvailablePlace;
+        }
+    };
+
+    class InjectingSockets : public MockedSockets {
+    public:
+        void ProcessEvent(MockedLws *mockedLws,
+                          enum lws_callback_reasons reason,
+                          std::vector<uint8_t> data = std::vector<uint8_t>()) override
+        {
+            auto &expectedEvent = EXPECTED_ORDER[m_eventNo];
+            ASSERT_EQ(reason, expectedEvent.reason);
+
+            while (m_injectionsMapNextEventIdx < m_injectionsMap->size() &&
+                   (*m_injectionsMap)[m_injectionsMapNextEventIdx].id == expectedEvent.id) {
+                MockedSockets::ProcessEvent(mockedLws,
+                                            (*m_injectionsMap)[m_injectionsMapNextEventIdx].reason);
+                ++m_injectionsMapNextEventIdx;
+            }
+
+            MockedSockets::ProcessEvent(mockedLws, reason, data);
+            m_eventNo++;
+        }
+
+        void ContextDestroy(LwsContext *lwsContext) noexcept override
+        {
+            /*
+             * In case of error caused by injected event this function is called changing the order
+             * of events.
+             */
+            m_eventNo = EXPECTED_ORDER.size() - 3;
+            assert(EXPECTED_ORDER[m_eventNo].id == 5);
+            assert(EXPECTED_ORDER[m_eventNo].reason == LWS_CALLBACK_CLIENT_CLOSED);
+            MockedSockets::ContextDestroy(lwsContext);
+        }
+
+        void Inject(const InjectionsMap &map)
+        {
+            m_injectionsMap = &map;
+            m_injectionsMapNextEventIdx = 0;
+            m_eventNo = 0;
+        }
+
+    private:
+        const std::vector<EventInfo> *m_injectionsMap;
+        size_t m_injectionsMapNextEventIdx = 0;
+        int m_eventNo = 0;
+    };
+
+    auto sockets = std::make_shared<InjectingSockets>();
+    Tunnel tunnel(sockets);
+    const std::string test = "sdfjisdjkfhdfjkghsdfkghdgkjhd";
+    std::vector<uint8_t> in, out;
+    out.assign(test.c_str(), test.c_str() + test.size());
+
+    size_t successes = 0;
+    auto testFn = [&] {
+        try {
+            tunnel.Connect(TestUrl());
+
+            tunnel.WriteBinary(out);
+            in = tunnel.ReadBinary();
+
+            tunnel.Disconnect();
+
+            if (out == in)
+                successes++;
+        } catch (const Unknown &) {
+        } catch (const InvalidState &) {
+        } catch (...) {
+            FAIL() << "Unexpected exception";
+        }
+    };
+
+    size_t injectionsNum = 0;
+    iterateAllInjections([&](const auto &injections) {
+        ++injectionsNum;
+        sockets->Inject(injections);
+        testFn();
+    });
+
+    std::cerr << successes << " successes out of " << injectionsNum << " injections\n";
+    EXPECT_GE(successes, 1);
+    EXPECT_LT(successes, injectionsNum);
+}
+
+TEST(TunnelMockedTests, CancellingFromOtherThread)
+{
+    class Transaction {
+    public:
+        Transaction() : m_tunnel(std::make_shared<DelayedSockets>()) {}
+
+        void PerformTransaction()
+        {
+            std::vector<uint8_t> in, out(42);
+            m_tunnel.Connect(TestUrl());
+            m_tunnel.WriteBinary(out);
+            in = m_tunnel.ReadBinary();
+            m_tunnel.Disconnect();
+        }
+
+        void Cancel() { m_tunnel.Cancel(); }
+
+    private:
+        Tunnel m_tunnel;
+    };
+
+    auto makeTransaction = [&] { return Transaction(); };
+    TestCancelFromTheOtherThread<Transaction>(
+        400, 40, makeTransaction, [](Transaction &transaction) {
+            transaction.PerformTransaction();
+        });
+}
diff --git a/tests/tunnel/auto_tests.h b/tests/tunnel/auto_tests.h
new file mode 100644 (file)
index 0000000..84c7536
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  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
+ */
+
+#pragma once
+
+#include "websockets.h"
+
+#include <gtest/gtest-param-test.h>
+#include <gtest/gtest.h>
+
+const std::string &TestUrl();
+
+struct MockedLws;
+struct MocketContext;
+
+class MockedSockets : public IWebsockets {
+protected:
+    virtual void ProcessEvent(MockedLws *mockedLws,
+                              enum lws_callback_reasons reason,
+                              std::vector<uint8_t> data = {});
+    LwsContext *CreateContext() noexcept override;
+    Lws *ClientConnect(LwsContext *lwsContext, const std::string &url) noexcept override;
+    bool Service(LwsContext *lwsContext) noexcept override;
+    bool CallbackOnWritable(Lws *lws) noexcept override;
+
+    // only this method can be called from a different thread
+    void CancelService(LwsContext *lwsContext) noexcept override;
+
+    bool FrameIsBinary(Lws *) const noexcept override { return true; }
+
+    bool IsFirstFragment(Lws *) const noexcept override { return true; }
+
+    bool IsFinalFragment(Lws *) const noexcept override { return true; }
+
+    int WriteBinary(Lws *lws, unsigned char *buf, size_t len) noexcept override;
+    void ContextDestroy(LwsContext *lwsContext) noexcept override;
+
+    void SetListener(IWebsocketsListener *listener) noexcept override { m_listener = listener; }
+
+protected:
+    IWebsocketsListener *m_listener = nullptr;
+};
+
+using MyTypes = ::testing::Types<MockedSockets>;
diff --git a/tests/tunnel/cert.pem b/tests/tunnel/cert.pem
new file mode 100644 (file)
index 0000000..2e7ce07
--- /dev/null
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF7zCCA9egAwIBAgIUU1GZzs9Zmn7O4mvKVLikr1Kn7ZIwDQYJKoZIhvcNAQEL
+BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM
+CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu
+eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y
+NDAxMjUxMjAyMDlaFw0zNDAxMjIxMjAyMDlaMIGGMQswCQYDVQQGEwJYWDESMBAG
+A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t
+cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU
+Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
+AoICAQC2XQx502TT8SOdptz5VrXP19ZH8Q5x1EUkgKH8R4GxijL1Lyou7AlsjABy
+XsQ998/5zFPy96jTt7TU04LzTK8tUpwisCfREFLNVJ4cQ0c5MjiOrDMTl3zTXjQ3
+kSBdIbmSPQO6VE4aHY4a8vFPjtQ8lpcwIc85ozRavSgstF9YZryk2ZYGySYdniNh
+PtZPW7s7iFGWyDSJM/1Jy/v4d5J0VJO+39cFnRTKJscOyJmZvuoZLxtuKB5LXZuc
+ACTw2NdzCKCm3kmZ/oRsh8FZ1cA9Vqved/6zciFg8qVN9hQcb1mf4qGIlnhbY7gh
+N+cKLkRh954MvUzLdOna04Wb9uIfvOSpWbakcFotaIQNzBS9qKeKn13HoXaE/gMm
+BpedQ+Z+W//mR4FNETCnY7WWbh5kPOzRROYx7NR7NUl3ybjB3PhCvcweuu6FiJeS
+Ns3c3qk2n0cGqAOCiKI4hJThGwVjfyHIO4HzdD4/EXsIeWAcwQVVG+LNyfLDX9Zb
+diqqsFAmXUNnq3I1VyJimNIU6eduxAF5WQm7DtIY6SAF0/ABIcghPFHlLgOT7Xt5
+U4blwkf4WWR8yez4NfnNDPnn5kDHUWT4XO0r0m7qjSW6YRpANLcxrYLtKqO1X0m4
+zcG5T4NJDsY70xbZdoaB24YmA6e1SiuNDtiIZDXM1GeduEwIowIDAQABo1MwUTAd
+BgNVHQ4EFgQUhocsNPPnuTGubj3QOz0G8hTJwFowHwYDVR0jBBgwFoAUhocsNPPn
+uTGubj3QOz0G8hTJwFowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
+AgEAawarPf2lWucDrZnrBjZmYNsz6AV3MPvtg1gdgMry/sU7pSd1NERGt6LFk6xf
+oyAX7R9EKZcMFQr031ilwvkPTtNoxeyqJohzqxKVAg2Y8vSQasNyUF2qkecCsJI+
+cSTF/ZuEjQhMFlsw6svN/iPBMKlxndt+8/ObTtJp136k3XX6wThrcg69WhoT/h3N
+qnpRQ+g6f5vgwnMQY/E4/HgX5C7pprxeccwXeLfUNQXkmzFrHTRcEXc+dLA+jOWf
+FG1gLTsPt56/7p5V89afz+THpqyuFP97xEBisr/NjCRbVHDWNPLk7dFq4JfaTevQ
+q/cj7K1wdf0Lk+dmtG4dJQnTOuWCiJ6YooCv1aS7fqwwzAbCeJFHskkStTFdH49Q
+unLi/ci8RYN2qHvonYYDI2iFVKtr6WjtiUHUwuyAt7j0mGKLtKXwQeA6r9Zenqb0
+Snu2dxlhII+kMQyz9CveXWzF+r3W8CHruDJ6jeIDWkdK+ApI9lgMQhzstxaiWc4I
+dHKE+BuW0DGWrFO+JsVAllbBs2kFAgITqjMkhzD7WGWEsQ4osaE3TWTBToTXsjCX
+iFZT8A4wHoqUmTZnHl4UQ8QXDdEWg5u3waWnpsddTY1vDDWbkkXz2S8bw08aA0Jv
+r5fXu96dE1lPFJBUfE4+T0w7eKwZmJhYh81Uwe6COTkKlbw=
+-----END CERTIFICATE-----
diff --git a/tests/tunnel/common_tests.cpp b/tests/tunnel/common_tests.cpp
new file mode 100644 (file)
index 0000000..5a3aeb3
--- /dev/null
@@ -0,0 +1,308 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  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
+ */
+
+#ifdef MANUAL_TESTS
+#include "manual_tests.h"
+#else
+#include "auto_tests.h"
+#endif
+#include "../get_random.h"
+#include "exception.h"
+#include "tunnel.h"
+
+#include <fstream>
+#include <thread>
+
+namespace {
+class Canceler {
+public:
+    explicit Canceler(Tunnel &tunnel) : m_tunnel(tunnel) {}
+
+    ~Canceler()
+    {
+        if (!m_quit)
+            Stop();
+    }
+
+    void Run()
+    {
+        assert(m_quit);
+        m_quit = false;
+        m_thread = std::thread([&] {
+            while (!m_quit)
+                m_tunnel.Cancel();
+        });
+    }
+
+    void Stop()
+    {
+        m_quit = true;
+        m_thread.join();
+    }
+
+private:
+    Tunnel &m_tunnel;
+    bool m_quit = true;
+    std::thread m_thread;
+};
+} // namespace
+
+/*
+ * Tests below are executed using two different websockets implementations:
+ * 1. TunnelTypedTests/0, where TypeParam = MockedSockets - using a fully mocked websockets
+ *    interface during automated tests.
+ * 2. TunnelTypedTests/0, where TypeParam = UnsafeSockets - using a dummy websockets server during
+ *    manual tests.
+ */
+template <typename T>
+struct TunnelTypedTests : public testing::Test {};
+
+TYPED_TEST_SUITE(TunnelTypedTests, MyTypes);
+
+TYPED_TEST(TunnelTypedTests, BigMessages)
+{
+    Tunnel tunnel(std::make_shared<TypeParam>());
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+
+    std::ifstream stream("/dev/urandom");
+    EXPECT_TRUE(stream.good());
+
+    for (unsigned i = 0; i < 10; ++i) {
+        auto size = get_random(100'000, 1'000'000);
+
+        std::vector<uint8_t> out(size);
+        std::vector<uint8_t> in;
+
+        stream.read(reinterpret_cast<char *>(out.data()), size);
+
+        EXPECT_NO_THROW(tunnel.WriteBinary(out));
+        EXPECT_NO_THROW(in = tunnel.ReadBinary());
+        EXPECT_EQ(in, out);
+    }
+}
+
+TYPED_TEST(TunnelTypedTests, NullContext)
+{
+    struct TestSockets : public TypeParam {
+        LwsContext *CreateContext() noexcept override { return nullptr; }
+    };
+
+    Tunnel tunnel(std::make_shared<TestSockets>());
+
+    EXPECT_THROW(tunnel.Connect(TestUrl()), Unknown);
+}
+
+TYPED_TEST(TunnelTypedTests, InvalidConnect)
+{
+    struct TestSockets : public TypeParam {
+        Lws *ClientConnect(LwsContext *, const std::string &) noexcept override { return nullptr; }
+    };
+
+    Tunnel tunnel(std::make_shared<TestSockets>());
+
+    EXPECT_THROW(tunnel.Connect(TestUrl()), InvalidState);
+}
+
+TYPED_TEST(TunnelTypedTests, EmptyWrite)
+{
+    Tunnel tunnel(std::make_shared<TypeParam>());
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+    EXPECT_THROW(tunnel.WriteBinary({}), Unknown);
+}
+
+TYPED_TEST(TunnelTypedTests, FailingCallbackOnWritable)
+{
+    struct TestSockets : public TypeParam {
+        bool CallbackOnWritable(Lws *) noexcept override { return false; }
+    };
+
+    Tunnel tunnel(std::make_shared<TestSockets>());
+    std::vector<uint8_t> out(16);
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+    EXPECT_THROW(tunnel.WriteBinary(out), InvalidState);
+    EXPECT_THROW(tunnel.WriteBinary(out), InvalidState);
+}
+
+TYPED_TEST(TunnelTypedTests, WriteWrongState)
+{
+    struct TestTunnel : public Tunnel {
+        using Tunnel::Tunnel;
+
+        bool HandleEvent(Lws *wsi,
+                         enum lws_callback_reasons reason,
+                         void *in,
+                         size_t len) noexcept override
+        {
+            if (reason == LWS_CALLBACK_CLIENT_WRITEABLE)
+                m_state = Tunnel::State::FAILED;
+            return Tunnel::HandleEvent(wsi, reason, in, len);
+        }
+    };
+
+    TestTunnel tunnel(std::make_shared<TypeParam>());
+    std::vector<uint8_t> out(16);
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+    EXPECT_THROW(tunnel.WriteBinary(out), InvalidState);
+}
+
+TYPED_TEST(TunnelTypedTests, WriteFailure)
+{
+    struct TestSockets : public TypeParam {
+        int WriteBinary(Lws *, unsigned char *, size_t) noexcept override { return 0; }
+    };
+
+    Tunnel tunnel(std::make_shared<TestSockets>());
+    std::vector<uint8_t> out(16);
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+    EXPECT_THROW(tunnel.WriteBinary(out), InvalidState);
+}
+
+TYPED_TEST(TunnelTypedTests, TruncatedWrite)
+{
+    struct TestSockets : public TypeParam {
+        int m_toWrite = 16;
+
+        int WriteBinary(Lws *, unsigned char *, size_t) noexcept override
+        {
+            if (m_toWrite <= 0)
+                return 0;
+
+            m_toWrite -= 4;
+            return 4;
+        }
+    };
+
+    auto sockets = std::make_shared<TestSockets>();
+    Tunnel tunnel(sockets);
+    std::vector<uint8_t> out(sockets->m_toWrite);
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+    EXPECT_NO_THROW(tunnel.WriteBinary(out));
+}
+
+TYPED_TEST(TunnelTypedTests, TruncatedWriteFailingCallbackOnWritable)
+{
+    struct TestSockets : public TypeParam {
+        bool CallbackOnWritable(Lws *wsi) noexcept override
+        {
+            m_calls++;
+            if (m_calls == 1)
+                return TypeParam::CallbackOnWritable(wsi);
+
+            return false;
+        }
+
+        int WriteBinary(Lws *, unsigned char *, size_t) noexcept override
+        {
+            if (m_toWrite <= 0)
+                return 0;
+
+            m_toWrite -= 4;
+            return 4;
+        }
+
+        int m_calls = 0;
+        int m_toWrite = 16;
+    };
+
+    auto sockets = std::make_shared<TestSockets>();
+    Tunnel tunnel(sockets);
+    std::vector<uint8_t> out(sockets->m_toWrite);
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+    EXPECT_THROW(tunnel.WriteBinary(out), InvalidState);
+}
+
+TYPED_TEST(TunnelTypedTests, ReadWrongState)
+{
+    struct TestTunnel : public Tunnel {
+        using Tunnel::Tunnel;
+
+        bool HandleEvent(Lws *wsi,
+                         enum lws_callback_reasons reason,
+                         void *in,
+                         size_t len) noexcept override
+        {
+            if (reason == LWS_CALLBACK_CLIENT_RECEIVE)
+                m_state = Tunnel::State::FAILED;
+            return Tunnel::HandleEvent(wsi, reason, in, len);
+        }
+    };
+
+    TestTunnel tunnel(std::make_shared<TypeParam>());
+    std::vector<uint8_t> out(16), in;
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+    EXPECT_NO_THROW(tunnel.WriteBinary(out));
+    EXPECT_THROW(tunnel.ReadBinary(), InvalidState);
+}
+
+TYPED_TEST(TunnelTypedTests, ConnectionError)
+{
+    class TestSockets : public TypeParam {
+    public:
+        bool Service(LwsContext *) noexcept override
+        {
+            char desc[] = "failure";
+            return TypeParam::m_listener->HandleEvent(nullptr,
+                                                      LWS_CALLBACK_CLIENT_CONNECTION_ERROR,
+                                                      static_cast<void *>(desc),
+                                                      strlen(desc));
+        }
+    };
+
+    Tunnel tunnel(std::make_shared<TestSockets>());
+
+    EXPECT_THROW(tunnel.Connect(TestUrl()), InvalidState);
+    EXPECT_THROW(tunnel.Connect(TestUrl()), InvalidState);
+}
+
+TYPED_TEST(TunnelTypedTests, Connection)
+{
+    Tunnel tunnel(std::make_shared<TypeParam>());
+
+    const std::string test = "sdfjisdjkfhdfjkghsdfkghdgkjhd";
+    std::vector<uint8_t> in, out;
+    out.assign(test.c_str(), test.c_str() + test.size());
+
+    EXPECT_NO_THROW(tunnel.Disconnect());
+
+    EXPECT_NO_THROW(tunnel.Cancel());
+    EXPECT_NO_THROW(tunnel.Cancel());
+    EXPECT_THROW(tunnel.Connect(TestUrl()), Cancelled);
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+
+    EXPECT_THROW(tunnel.Connect(TestUrl()), InvalidState);
+    EXPECT_NO_THROW(tunnel.WriteBinary(out));
+    EXPECT_NO_THROW(in = tunnel.ReadBinary());
+
+    EXPECT_EQ(in, out);
+
+    out.resize(out.size() / 2);
+    EXPECT_NO_THROW(tunnel.WriteBinary(out));
+    EXPECT_NO_THROW(in = tunnel.ReadBinary());
+
+    EXPECT_EQ(in, out);
+
+    EXPECT_NO_THROW(tunnel.Disconnect());
+    EXPECT_NO_THROW(tunnel.Disconnect());
+}
diff --git a/tests/tunnel/manual_tests.cpp b/tests/tunnel/manual_tests.cpp
new file mode 100644 (file)
index 0000000..a370913
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  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 "../test_cancel_from_the_other_thread.h"
+#include "exception.h"
+#include "manual_tests.h"
+
+#include <cassert>
+
+namespace {
+
+const std::string TUNNEL_SERVER_DOMAINS[] = {"cable.ua5v.com", "cable.auth.com"};
+
+struct UnsafeTunnel : public Tunnel {
+    UnsafeTunnel() : Tunnel(std::make_shared<UnsafeSockets>()) {}
+};
+
+} // namespace
+
+Lws *UnsafeSockets::ClientConnect(LwsContext *context, const std::string &url) noexcept
+{
+    static constexpr auto UNSAFE_FLAGS = LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED |
+        LCCSCF_ALLOW_INSECURE | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
+
+    return Websockets::DoClientConnect(context, url, UNSAFE_FLAGS);
+}
+
+class Config {
+private:
+    /*
+     * Default setting for tests running on emulator and websocket_server.py server running on the
+     * host. Can be overriden by using "--server=" argument.
+     */
+    Config() : m_testServer("10.0.2.2:7890") {}
+
+public:
+    static Config &Instance()
+    {
+        static Config instance;
+        return instance;
+    }
+
+    void ParseArguments(int argc, char *argv[])
+    {
+        const char *SERVER_PREFIX = "--server=";
+        for (int i = 1; i < argc; ++i)
+            if (strncmp(SERVER_PREFIX, argv[i], strlen(SERVER_PREFIX)) == 0)
+                m_testServer = argv[i] + strlen(SERVER_PREFIX);
+    }
+
+    const std::string &TestUrl() const
+    {
+        static const std::string TEST_URL = std::string("wss://") + m_testServer;
+        return TEST_URL;
+    }
+
+    ~Config() = default;
+
+private:
+    std::string m_testServer;
+};
+
+const std::string &TestUrl() { return Config::Instance().TestUrl(); }
+
+/*
+ * TunnelDummyServerTests require a running websocket server implementing "echo" functionality.
+ *
+ * Unset http_proxy env variable to get rid of "lws_set_proxy: http_proxy needs to be ads:port"
+ * error log.
+ */
+TEST(TunnelDummyServerTests, WrongUrl)
+{
+    UnsafeTunnel tunnel;
+    std::string routingId = "123456";                          // 3B
+    std::string tunnelId = "0123456789abcdef0123456789abcdef"; // 16B
+    std::ostringstream oss;
+    const std::string test = "sdfjisdjkfhdfjkghsdfkghdgkjhd";
+    std::vector<uint8_t> in, out;
+    out.assign(test.c_str(), test.c_str() + test.size());
+
+    oss.str("");
+    oss.clear();
+    oss << "ws://" << TUNNEL_SERVER_DOMAINS[0] << "/cable/connect/" << routingId << "/" << tunnelId;
+    EXPECT_THROW(tunnel.Connect(oss.str()), InvalidState);
+    EXPECT_NO_THROW(tunnel.Disconnect());
+
+    oss.str("");
+    oss.clear();
+    oss << "wss://a" << TUNNEL_SERVER_DOMAINS[0] << "/cable/connect/" << routingId << "/"
+        << tunnelId;
+    // May fail at different stage with different error depending on the network setup
+    EXPECT_THROW(tunnel.Connect(oss.str()), ExceptionBase);
+    EXPECT_NO_THROW(tunnel.Disconnect());
+
+    EXPECT_THROW(tunnel.WriteBinary(out), InvalidState);
+    EXPECT_THROW(tunnel.ReadBinary(), InvalidState);
+}
+
+TEST(TunnelDummyServerTests, TooBigMessages)
+{
+    UnsafeTunnel tunnel;
+
+    EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+
+    std::vector<uint8_t> out(1'048'576);
+    EXPECT_NO_THROW(tunnel.WriteBinary(out));
+    EXPECT_EQ(tunnel.ReadBinary(), out);
+
+    out.push_back(0);
+    EXPECT_NO_THROW(tunnel.WriteBinary(out));
+    EXPECT_THROW(tunnel.ReadBinary(), InvalidState);
+}
+
+TEST(TunnelDummyServerTests, CancellingFromOtherThread)
+{
+    class Transaction {
+    public:
+        void PerformTransaction()
+        {
+            std::vector<uint8_t> in, out(42);
+            m_tunnel.Connect(TestUrl());
+            m_tunnel.WriteBinary(out);
+            in = m_tunnel.ReadBinary();
+            m_tunnel.Disconnect();
+        }
+
+        void Cancel() { m_tunnel.Cancel(); }
+
+    private:
+        UnsafeTunnel m_tunnel;
+    };
+
+    auto makeTransaction = [&] { return Transaction(); };
+    TestCancelFromTheOtherThread<Transaction>(
+        400, 40, makeTransaction, [](Transaction &transaction) {
+            transaction.PerformTransaction();
+        });
+}
+
+int main(int argc, char *argv[])
+{
+    try {
+        ::testing::InitGoogleTest(&argc, argv);
+
+        Config::Instance().ParseArguments(argc, argv);
+
+        return RUN_ALL_TESTS();
+    } catch (...) {
+        return 1;
+    }
+}
diff --git a/tests/tunnel/manual_tests.h b/tests/tunnel/manual_tests.h
new file mode 100644 (file)
index 0000000..b7275a6
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  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
+ */
+
+#pragma once
+
+#include "tunnel.h"
+
+#include <gtest/gtest-param-test.h>
+#include <gtest/gtest.h>
+#include <string>
+
+struct UnsafeSockets : public Websockets {
+    Lws *ClientConnect(LwsContext *context, const std::string &url) noexcept override;
+};
+
+using MyTypes = ::testing::Types<UnsafeSockets>;
+
+const std::string &TestUrl();
diff --git a/tests/tunnel/manual_tests.py b/tests/tunnel/manual_tests.py
new file mode 100755 (executable)
index 0000000..a986c11
--- /dev/null
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+
+from threading import Thread
+import subprocess
+import websockets
+import asyncio
+import binascii
+import time
+import ssl
+import logging
+import os
+import sys
+from datetime import datetime, timezone
+import re
+
+DIR = os.path.dirname(__file__) + "/"
+
+async def echo(websocket, path):
+    logging.info("A client just connected at " + path)
+
+    try:
+        async for message in websocket:
+            logging.info("Received message from client: " + binascii.hexlify(message[:64]).decode("ascii"))
+
+            await websocket.send(message)
+            logging.info("Echo response sent")
+
+    except websockets.exceptions.ConnectionClosed as e:
+       logging.info("A client just disconnected")
+
+def delay(path, request_headers):
+    if path.find("slow") != -1:
+        time.sleep(0.2)
+
+def get_host_ip():
+    # find host ip by sdb port 26101
+    # e.g. tcp 0 0 10.0.0.1:52860 10.0.0.2:26101 ESTABLISHED
+    result = subprocess.run(["netstat", "--inet","--tcp","--numeric"], capture_output=True)
+    result.check_returncode()
+    for line in result.stdout.decode().splitlines():
+        match = re.search(r'.+ ([^ :]+):.+:26101 +ESTABLISHED', line)
+        if match:
+            ip = match.group(1)
+            print(f"Found sdb connection running on {ip}")
+            return ip
+
+    print("No active sdb connection found")
+    return "127.0.0.1"
+
+async def start_server():
+    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    ssl_cert = DIR + "cert.pem"
+    ssl_key = DIR + "privkey.pem"
+    ssl_context.load_cert_chain(ssl_cert, keyfile=ssl_key)
+    PORT = 7890
+    HOST_IP = get_host_ip()
+
+
+    msg = f"Starting websocket server on {HOST_IP}:{PORT}..."
+    logging.info(msg)
+
+    print(msg)
+    start_server = websockets.serve(echo, HOST_IP, PORT, process_request=delay, ssl=ssl_context)
+
+    return await(start_server)
+
+def background_loop(event_loop):
+    logging.basicConfig(filename=DIR + "dummy_server.log", level=logging.INFO)
+    logging.info("Background loop started")
+
+    asyncio.set_event_loop(event_loop)
+    event_loop.run_forever()
+
+    logging.info("Background loop finished")
+    print("...stopped")
+
+async def stop_server():
+    asyncio.get_running_loop().stop()
+
+if __name__ == "__main__":
+    event_loop = asyncio.new_event_loop()
+    thread = Thread(target=background_loop, args=(event_loop,))
+    thread.start()
+
+    asyncio.run_coroutine_threadsafe(start_server(), event_loop).result()
+
+    print("...started")
+
+    print()
+    subprocess.run(["sdb", "root", "on"]).check_returncode()
+    now = datetime.now(timezone.utc).strftime("%c %Z")
+    subprocess.run(["sdb", "shell", "date", f"--set={now}"]).check_returncode()
+    subprocess.run(["sdb", "shell", "webauthn-ble-manual-tunnel-tests.sh"] + sys.argv[1:]).check_returncode()
+    print()
+
+    print("Stopping the server...")
+    asyncio.run_coroutine_threadsafe(stop_server(), event_loop)
+    thread.join()
diff --git a/tests/tunnel/manual_tests.sh.in b/tests/tunnel/manual_tests.sh.in
new file mode 100644 (file)
index 0000000..5606c78
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+set -exuo pipefail
+
+HOST_IP=`route -n | grep --max-count=1 'UG[ \t]' | awk '{print $2}'`
+
+@BIN_DIR@/@TARGET_WEBAUTHN_BLE_MANUAL_TUNNEL_TESTS@ --server=$HOST_IP:7890 "$@"
\ No newline at end of file
diff --git a/tests/tunnel/privkey.pem b/tests/tunnel/privkey.pem
new file mode 100644 (file)
index 0000000..4153fd1
--- /dev/null
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC2XQx502TT8SOd
+ptz5VrXP19ZH8Q5x1EUkgKH8R4GxijL1Lyou7AlsjAByXsQ998/5zFPy96jTt7TU
+04LzTK8tUpwisCfREFLNVJ4cQ0c5MjiOrDMTl3zTXjQ3kSBdIbmSPQO6VE4aHY4a
+8vFPjtQ8lpcwIc85ozRavSgstF9YZryk2ZYGySYdniNhPtZPW7s7iFGWyDSJM/1J
+y/v4d5J0VJO+39cFnRTKJscOyJmZvuoZLxtuKB5LXZucACTw2NdzCKCm3kmZ/oRs
+h8FZ1cA9Vqved/6zciFg8qVN9hQcb1mf4qGIlnhbY7ghN+cKLkRh954MvUzLdOna
+04Wb9uIfvOSpWbakcFotaIQNzBS9qKeKn13HoXaE/gMmBpedQ+Z+W//mR4FNETCn
+Y7WWbh5kPOzRROYx7NR7NUl3ybjB3PhCvcweuu6FiJeSNs3c3qk2n0cGqAOCiKI4
+hJThGwVjfyHIO4HzdD4/EXsIeWAcwQVVG+LNyfLDX9ZbdiqqsFAmXUNnq3I1VyJi
+mNIU6eduxAF5WQm7DtIY6SAF0/ABIcghPFHlLgOT7Xt5U4blwkf4WWR8yez4NfnN
+DPnn5kDHUWT4XO0r0m7qjSW6YRpANLcxrYLtKqO1X0m4zcG5T4NJDsY70xbZdoaB
+24YmA6e1SiuNDtiIZDXM1GeduEwIowIDAQABAoICAA8andHpREyZiE2iaGLuX5ib
+U9AZkwyyfBeN39y5P3Co9ZeBFIlWW2F2JwNR/gbz43HZDortIDqI2J1X91yXWVrz
+oKLu3B1gsLr46y+EEv4VvsFyWbihr2ECSGjhyEBubqRCEXD5Uo6vK/nnbT0do05x
+WoxOAI/RiKCLUiyBs/Pqbm1o6BgfyNxjIKMJRU7FVjiUDc3jPxauC6h1EKV/oc2o
+w6PnfjOk0vf7RjlfO/mT6mezkx4IjJnJYv4EtQqNqyqH6GZpusbaYbVreQQrxARQ
+jWObpKjQJxuUfUR7wSapwk6PwrlBXS6Uj8eQjb42HsvOanS7qQSg5k2Nf19vj0LV
+4xESmYEMAc29pDJg74HULrwwzBn+PHOhRQ0Z89lJOPXWb63ocF6U2RhyaVv4tKA0
+CiD8GrOYAP1xg/eVq6qFOSG1u0PshVLBwlfZinXCQGFBCI8Z8P639f5/kQfch0Yh
+odX8Q/sj3j8k+Xr/ZVkhO4DRmVWHcMhYyaOLL5cTypVCq5SpQ+XwjErVGAODXy39
+Hjk8TPx1VcKVP7eSpTjHidGDssrL8OqI6i6j4mFSIvTRT+uKVlz9I1nlU9AnfI18
+YP0SiPOlGOGIPB0yqQ0e6eKbBGiLaciV8/BjYj8mxThNZowSIjjk9EZaO6ogtW+c
+boVrgos34DK+/UAbYr5BAoIBAQDamg3VVCWxtnoQgZG9e8qmHnOEy/ZXiD/fcA5q
+asz5CpbDSt3DdFRRbhMAcn4qucuw6QX2oMMNMPT5m3mSOrwjRWkeHX57T9bKIBQP
+hYVlTOD3uRn92VmLLt9QY8DagZmm7lFBKlUH0pJZrFGJJt0uaZJpzt8jnhusMfd8
+7TvtSJM3dAcFuM3twep7Ero7X56C7uLjoMWa5bWyIZZhcq4sKzFiBLKlHLSQy1MQ
+YJ9m19RN7Xz+i7dzjDYtQvKW6c3jnTZd3796V6WOwRmPVKasNV1XyUT5oYKWV9Jo
+kYKk2F2Z3lUjt8QJ4faPizP2BVyNhlgQ44pYRhcwzaOudyfJAoIBAQDVj+RCC+u2
+xTK1rZcrX0e6ofmB79KwEIQ8ibyJ1L/UR6tBqtRxRErxlEyI6D21HyEZZLZy4mfE
+EXCb00tMR347UY5ny5mn4qGU6VzEgkpsbnfsI1R+ZJxmTqJTuAnq6GcjGOIIZfwA
+clL05qy1ubH2YAzInU/EyH9BmmBZroRMGdMKmsZ6r3FjnK2gudrOGNKBA/NYQL2A
+paWqt6b87BbusvWhIC5Bap+C81SStlkCYan5ghbkYXkwlaEHcIV4/5U2Au0IAyIb
+gkytAbUQHAMuAFfISHyfEjvaRq7GaIAxWOZtmkvFJZdBYu0Wr3fKHhY+VkNsIZL3
+lutxa5xv8jsLAoIBAGNApx9BAYxayblM2xspZa1fDhwxbzv3BOkyO5ldvIsmn6U0
+DNR9sPr3+3Csi1Ri33UHmEmOXIFUxSW/zcbzHBD3pu6hfZgHfAJx5inV+ecwrMRN
+KtvzH5DuSz54zEtppJYChqoLymeu1/wXHT4kgzBbhrq7ztyhIPdiCHiUTi1CUnVP
+HREgA3/8F7ahniTvGU1hoWqwyIe33HNwhJLiOuqbAeT7NRF6oxFK///jnvxW6nIn
+biU00qqH8uFCEFss9OpvqwucV5f4y5axXiWRzctKv4LoQ4UELAKPBV2tE6A9DOKb
+7BQ3j67dBdHO5lzBunAXm0yvw1SpHkZuV+8S8uECggEBAJOINe2SgqHelOuBIj9q
+MQkzQESNQoUyw8d2d3LYMDUb1akltGETBprhEgY6OfbXrGv4cTnoQjrmuV5Ml2X4
+tbGQ48m8k0exfmJ63AU85OEHZ30P4xz1sD3U8LrZFrDbH/TVCcE3guFkGP7qJEHe
+KJzFOc+VLTgKMi9F0G3j660O4gYfaiHy9WgVIrU373oVF8bczc0X3FH1HP9Uo91x
+013O73UB4DJ8z/kM1E4N/mtwwJWHUv0ugSQZGgcjqnEuTwvi9ZBlabiLSSDzXvvj
+/iZXQk0wFjdlx0dBMRgLtRNiEL2Cq/ljwJwBFTxot14/bqpduXIHt5mm/rt51bZY
+JoUCggEBAIE9X8Jwx3jkl+XBPGl5v4x3GvORivXfck2DNmcRIHtgtt6kd40OilTt
+NK+am2FApFXucAfRUksLRMQ5CcfPmNhHI4WibosDD8cN+WN/IkYZwKxuTm+bmJy5
+ypEuLglXOVoAy8+i1ZYTwMtkk7R5thfJp59xh1bS5lFXlUbxWqO/PSEouga9XODx
+IHg7uupM/SdOm+/SvCJJUm9aDlUMxFgjh1I+uLFrzrzln5qwWL4P/KTkMDpYR20o
+XON9NdTtMjVChvSiKsTWDuEI0MenfH/J7+noHWR1MSTqsCx0uMrKmS9Cy2cr9SbN
+swhUmESLJNON4QPmtGAMcN4OTE30eZQ=
+-----END PRIVATE KEY-----