Manual and automated tests included.
Change-Id: Ie921f938b55a770a76cd68d45b888b6665807c46
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)
%manifest %{name}-manual-tests.manifest
%license LICENSE
%{_bindir}/%{name}-manual-tests
+%{_bindir}/%{name}-manual-tunnel-tests
+%{_bindir}/%{name}-manual-tunnel-tests.sh
${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)
#pragma once
+#include "log/log.h"
+
#include <stdexcept>
#include <string>
#include <type_traits>
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")
LogPedantic(__VA_ARGS__); \
} catch (...) { \
}
+
+#define LOGGED_THROW(exceptionType, msg) \
+ do { \
+ TRY_LOG_ERROR("Throwing exception: " << msg); \
+ throw exceptionType(msg); \
+ } while (false)
} catch (const abi::__forced_unwind &) {
throw;
} catch (...) {
+ TRY_LOG_ERROR("Mutex lock failed");
result = WAUTHN_ERROR_UNKNOWN;
}
});
} catch (const abi::__forced_unwind &) {
throw;
} catch (...) {
+ TRY_LOG_ERROR("Response callback has thrown an exception");
}
}
}
--- /dev/null
+/*
+ * 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");
+ }
+}
--- /dev/null
+/*
+ * 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;
+};
+
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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];
+};
LINK_DIRECTORIES(${TESTS_DEPS_LIBRARY_DIRS})
+# unit tests
SET(UNIT_TESTS_SOURCES
${WEBAUTHN_BLE_SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
${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})
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
)
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})
--- /dev/null
+dummy_server.log
--- /dev/null
+/*
+ * 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 = ↦
+ 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();
+ });
+}
--- /dev/null
+/*
+ * 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>;
--- /dev/null
+-----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-----
--- /dev/null
+/*
+ * 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());
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
--- /dev/null
+#!/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()
--- /dev/null
+#!/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
--- /dev/null
+-----BEGIN PRIVATE KEY-----
+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
+swhUmESLJNON4QPmtGAMcN4OTE30eZQ=
+-----END PRIVATE KEY-----