From 666fe6e801370f69303b8a15cb42c6dd10d8e0b0 Mon Sep 17 00:00:00 2001 From: Francois Michaut Date: Sun, 25 Oct 2026 12:49:28 -0400 Subject: [PATCH] Misc performance improvements and bugfixes - Fix CI vcpkg - Fix TLS callback issues and allow for CTX or Socket level callbacks - Made Socket virtual to ensure behavior if Tls called from Socket ref - Portable m_domain, m_protocol and m_type inference - Better move correctness - Better closing semantics, especially for TlsSockets - Fixed read performance and correctness - Handles windows error messages + thread safety --- .clang-tidy | 1 + .github/workflows/cmake-multi-platform.yml | 6 + .github/workflows/code_scanning.yml | 4 +- .github/workflows/download-cache.yml | 22 -- .github/workflows/windows-vcpkg/action.yml | 4 +- CMakeLists.txt | 12 +- include/CppSockets/Socket.hpp | 31 ++- include/CppSockets/Tls/Context.hpp | 7 +- include/CppSockets/Tls/Socket.hpp | 34 +-- private/CppSockets/SslMacros.hpp | 2 +- source/Address.cpp | 9 +- source/Socket.cpp | 253 ++++++++++++++------- source/SocketInit.cpp | 2 +- source/Tls/Callbacks.cpp | 74 ++++++ source/Tls/Context.cpp | 47 +--- source/Tls/Socket.cpp | 126 +++++----- 16 files changed, 401 insertions(+), 233 deletions(-) delete mode 100644 .github/workflows/download-cache.yml create mode 100644 source/Tls/Callbacks.cpp diff --git a/.clang-tidy b/.clang-tidy index 1e15b84..4bb3b92 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -6,6 +6,7 @@ Checks: ' -fuchsia*, -google-readability-todo, -cppcoreguidelines-init-variables, +-cppcoreguidelines-owning-memory, -cppcoreguidelines-pro-type-reinterpret-cast, -llvm-namespace-comment, -readability-implicit-bool-conversion, diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 1ca5d17..ac17b73 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -10,6 +10,9 @@ on: jobs: build: runs-on: ${{ matrix.os }} + permissions: + contents: read + actions: write strategy: # ensure we don't stop after 1 failure to always have a complete picture of what is failing @@ -127,6 +130,9 @@ jobs: needs: 'build' runs-on: ubuntu-latest if: always() && github.event_name == 'pull_request' + permissions: + contents: read + pull-requests: write steps: - name: Checkout Code diff --git a/.github/workflows/code_scanning.yml b/.github/workflows/code_scanning.yml index 630502d..3f63103 100644 --- a/.github/workflows/code_scanning.yml +++ b/.github/workflows/code_scanning.yml @@ -60,7 +60,7 @@ jobs: security-events: write steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v7 - name: flawfinder_scan uses: david-a-wheeler/flawfinder@2.0.19 @@ -93,7 +93,7 @@ jobs: - name: VCPKG Install (Windows) uses: ./.github/workflows/windows-vcpkg with: - key: ${{ runner.os }}-${{ env.config }} + key: vcpkg-cache-${{ runner.os }} - name: Configure CMake run: cmake -B ${{ env.build }} -DCMAKE_BUILD_TYPE=${{ env.config }} diff --git a/.github/workflows/download-cache.yml b/.github/workflows/download-cache.yml deleted file mode 100644 index ad436ac..0000000 --- a/.github/workflows/download-cache.yml +++ /dev/null @@ -1,22 +0,0 @@ -on: - workflow_dispatch: - inputs: - cache-key: - description: 'The key of the cache to retrieve' - required: true - -jobs: - download_and_upload_cache: - runs-on: ubuntu-latest - steps: - - name: Restore cache - uses: actions/cache@v4 - with: - path: ./cache - key: ${{ inputs.cache-key }} - - - name: Upload cached data as artifact - uses: actions/upload-artifact@v3 - with: - name: cached-data-artifact - path: ./cache diff --git a/.github/workflows/windows-vcpkg/action.yml b/.github/workflows/windows-vcpkg/action.yml index 47e81e2..8da7a1d 100644 --- a/.github/workflows/windows-vcpkg/action.yml +++ b/.github/workflows/windows-vcpkg/action.yml @@ -16,21 +16,19 @@ runs: - name: Fetch VCPKG Cache (Windows) id: fetch-vcpkg-cache - if: runner.os == 'Windows' uses: actions/cache/restore@v6 with: key: ${{ inputs.key }}-vcpkg-${{ hashFiles('vcpkg.json') }} path: ${{ env.VCPKG_CACHE }} - name: VCPKG Install (Windows) - if: runner.os == 'Windows' shell: powershell run: | echo "CMAKE_TOOLCHAIN_FILE=${env:VCPKG_ROOT}\scripts\buildsystems\vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Append vcpkg install - name: Always Save VCPKG Cache (Windows) - if: always() && runner.os == 'Windows' && steps.fetch-vcpkg-cache.outputs.cache-hit != 'true' + if: always() && steps.fetch-vcpkg-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v6 with: key: ${{ steps.fetch-vcpkg-cache.outputs.cache-primary-key }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 3115408..201343e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Sun Aug 28 19:26:51 2022 Francois Michaut -## Last update Thu Jul 2 01:56:37 2026 Francois Michaut +## Last update Sat Oct 24 23:32:08 2026 Francois Michaut ## ## CMakeLists.txt : CMake to build the CppSockets library ## @@ -21,6 +21,7 @@ project(LibCppSockets VERSION 0.1.0 LANGUAGES C CXX) configure_file(include/CppSockets/Version.hpp.in include/CppSockets/Version.hpp) add_library(cppsockets + source/Tls/Callbacks.cpp source/Tls/Certificate.cpp source/Tls/Context.cpp source/Tls/Utils.cpp @@ -40,7 +41,14 @@ target_include_directories(cppsockets ${PROJECT_SOURCE_DIR}/private ) -target_compile_definitions(cppsockets PRIVATE CPPSOCKETS_DLL_EXPORTS=1) +if(WIN32) + target_compile_definitions(cppsockets PRIVATE CPPSOCKETS_DLL_EXPORTS=1) +endif() + +target_compile_options(cppsockets PRIVATE + $<$,$>:-Wall -Wextra> + $<$:/W4> +) find_package(OpenSSL 3.0 COMPONENTS SSL) target_link_libraries(cppsockets OpenSSL::SSL) diff --git a/include/CppSockets/Socket.hpp b/include/CppSockets/Socket.hpp index 6b4bcc2..806cae1 100644 --- a/include/CppSockets/Socket.hpp +++ b/include/CppSockets/Socket.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sat Jan 15 01:17:42 2022 Francois Michaut -** Last update Wed Jul 1 14:20:12 2026 Francois Michaut +** Last update Sun Oct 25 11:38:13 2026 Francois Michaut ** ** Socket.hpp : Portable C++ socket class */ @@ -37,7 +37,7 @@ namespace CppSockets { Socket(); Socket(int domain, int type, int protocol); Socket(RawSocketType fd, bool connected); - ~Socket(); + virtual ~Socket(); // TODO Maybe enable copy with dup(2) ? Socket(const Socket &other) = delete; @@ -45,18 +45,19 @@ namespace CppSockets { auto operator=(const Socket &other) -> Socket & = delete; auto operator=(Socket &&other) noexcept -> Socket &; - auto read(std::size_t len = -1) -> std::string; - auto read(char *buff, std::size_t size) -> std::size_t; - auto write(const std::string &buff) -> std::size_t; - auto write(const char *buff, std::size_t len) -> std::size_t; + virtual void close(); + + virtual auto read(std::size_t len = -1) -> std::string; + virtual auto read(char *buff, std::size_t size) -> std::size_t; + virtual auto write(std::string_view buff) -> std::size_t; + virtual auto write(const char *buff, std::size_t len) -> std::size_t; auto set_reuseaddr(bool value) -> int; auto getsockopt(int level, int optname, SockOptType *optval, socklen_t *optlen) -> int; auto setsockopt(int level, int optname, const SockOptType *optval, socklen_t optlen) -> int; - void close(); - auto connect(const IEndpoint &endpoint) -> int; - auto connect(const std::string &addr, uint16_t port) -> int; + virtual auto connect(const IEndpoint &endpoint) -> int; + virtual auto connect(const std::string &addr, uint16_t port) -> int; auto bind(const IEndpoint &endpoint) -> int; auto bind(const std::string &addr, uint16_t port) -> int; @@ -74,15 +75,23 @@ namespace CppSockets { [[nodiscard]] auto connected() const -> bool { return m_is_connected; } static auto get_errno() -> int; - static auto strerror(int err) -> char *; - static auto strerror() -> char *; + static auto strerror(int err) -> std::string; + static auto strerror() -> std::string; protected: + static void throw_errno_error(std::string message); + static void throw_errno_error(std::string message, int err); + static auto getsockopt(int fd, int level, int optname, SockOptType *optval, socklen_t *optlen) -> int; + static auto getsockopt(int fd, int level, int optname, SockOptType *optval, socklen_t optlen) -> int { return Socket::getsockopt(fd, level, optname, optval, &optlen); } + auto bind(std::uint32_t addr, uint16_t port) -> int; void set_connected(bool status) { m_is_connected = status; }; + void create_socket(); + + virtual auto acceptImpl(void *addr_out) -> Socket *; private: int m_domain = 0; int m_type = 0; diff --git a/include/CppSockets/Tls/Context.hpp b/include/CppSockets/Tls/Context.hpp index f7b17f7..bd8cd4a 100644 --- a/include/CppSockets/Tls/Context.hpp +++ b/include/CppSockets/Tls/Context.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Aug 20 14:13:44 2025 Francois Michaut -** Last update Tue Jun 30 08:19:16 2026 Francois Michaut +** Last update Thu Oct 22 23:23:49 2026 Francois Michaut ** ** Context.hpp : Context for TLS sockets */ @@ -22,7 +22,7 @@ namespace CppSockets { TlsContext(SSL_CTX_ptr ptr); TlsContext(SSL_CTX *ptr, bool own = true); - TlsContext(const TlsContext &other) { *this = other; } + TlsContext(const TlsContext &other); TlsContext(TlsContext &&other) noexcept; auto operator=(const TlsContext &other) -> TlsContext &; auto operator=(TlsContext &&other) noexcept -> TlsContext &; @@ -41,8 +41,5 @@ namespace CppSockets { private: SSL_CTX_ptr m_ptr; bool m_own = true; - TlsVerifyCallback m_verify_callback; - - friend struct TlsContext_Accessor; }; } diff --git a/include/CppSockets/Tls/Socket.hpp b/include/CppSockets/Tls/Socket.hpp index ae4027a..1aadd97 100644 --- a/include/CppSockets/Tls/Socket.hpp +++ b/include/CppSockets/Tls/Socket.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Sep 14 20:51:23 2022 Francois Michaut -** Last update Fri Aug 22 21:55:50 2025 Francois Michaut +** Last update Sun Oct 25 12:00:59 2026 Francois Michaut ** ** SecureSocket.hpp : TLS socket wrapper using openssl */ @@ -20,30 +20,31 @@ namespace CppSockets { // TODO add more TLS-related functions class TlsSocket : public Socket { public: - TlsSocket() = default; - // TODO: Constructor allowing application to reuse SSL_CTX objects - // (Maybe even a different TLS_CTX class to manage them ?) + TlsSocket() = default; // TODO: Null-init m_ssl, would segfault on any function call. Find a safer way. TlsSocket(int domain, int type, int protocol, TlsContext ctx = {}); explicit TlsSocket(Socket &&other, TlsContext ctx = {}); explicit TlsSocket(Socket &&other, SSL_ptr ssl = nullptr); explicit TlsSocket(RawSocketType fd, SSL_ptr ssl = nullptr); - ~TlsSocket() noexcept; + ~TlsSocket() noexcept override; TlsSocket(const TlsSocket &other) = delete; TlsSocket(TlsSocket &&other) noexcept; auto operator=(const TlsSocket &other) -> TlsSocket & = delete; auto operator=(TlsSocket &&other) noexcept -> TlsSocket &; - void close(); + void close() override { this->close(false); }; + bool close(bool force); - auto read(std::size_t len = -1) -> std::string; - auto read(char *buff, std::size_t size) -> std::size_t; - auto write(std::string_view buff) -> std::size_t { return this->write(buff.data(), buff.size()); }; - auto write(const char *buff, std::size_t len) -> std::size_t; + // read() with no length reads until the peer closes; use read(N) when reading from untrusted peers + auto read(std::size_t len = -1) -> std::string override; + auto read(char *buff, std::size_t size) -> std::size_t override; + auto write(std::string_view buff) -> std::size_t override { return this->write(buff.data(), buff.size()); }; + auto write(const char *buff, std::size_t len) -> std::size_t override; - void set_verify(int mode, SSL_verify_cb verify_callback = nullptr); + void set_verify(int mode, TlsVerifyCallback verify_callback = nullptr); + void set_verify_depth(int depth); void set_certificate(const std::string &cert_path, const std::string &pkey_path); - auto connect(const IEndpoint &endpoint) -> int; + auto connect(const IEndpoint &endpoint) -> int override; auto accept(void *addr_out, TlsContext ctx) -> std::unique_ptr; auto accept(void *addr_out = nullptr) -> std::unique_ptr { return accept(addr_out, m_ctx); } @@ -54,11 +55,18 @@ namespace CppSockets { [[nodiscard]] auto get_peer_cert() const -> const X509_ptr & { return m_peer_cert; } [[nodiscard]] auto tls_strerror(int ret) -> std::string; + protected: + auto acceptImpl(void *addr_out) -> TlsSocket * override { return acceptImpl(addr_out, m_ctx); }; + auto acceptImpl(void *addr_out, TlsContext ctx) -> TlsSocket *; + + void throw_tls_error(std::string message); + void throw_tls_error(std::string message, int ret); private: TlsContext m_ctx; SSL_ptr m_ssl; X509_ptr m_peer_cert; - void check_for_error(const std::string &error_msg, int ret); + bool m_should_shutdown = false; + void check_for_error(std::string error_msg, int ret); }; } diff --git a/private/CppSockets/SslMacros.hpp b/private/CppSockets/SslMacros.hpp index d313c74..1513c32 100644 --- a/private/CppSockets/SslMacros.hpp +++ b/private/CppSockets/SslMacros.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Aug 20 16:54:02 2025 Francois Michaut -** Last update Thu Jul 2 03:01:00 2026 Francois Michaut +** Last update Thu Oct 22 22:48:57 2026 Francois Michaut ** ** SslMacros.hpp : Private Macros to define SSL wrappers */ diff --git a/source/Address.cpp b/source/Address.cpp index 388dd46..0d49030 100644 --- a/source/Address.cpp +++ b/source/Address.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sun Feb 13 22:03:32 2022 Francois Michaut -** Last update Wed Jul 1 14:19:53 2026 Francois Michaut +** Last update Sun Oct 25 12:47:04 2026 Francois Michaut ** ** Address.cpp : Implementation of generic Address classes & functions */ @@ -28,6 +28,11 @@ namespace CppSockets { auto IEndpoint::from_string(std::string endpoint) -> std::shared_ptr { // TODO: Support IPv6 std::size_t colon_pos = endpoint.find_last_of(':'); + + if (colon_pos == std::string::npos) { + throw std::runtime_error("Invalid Endpoint"); + } + std::stringstream ss{std::move(endpoint)}; std::string ip_part(colon_pos, '\0'); // 0-init str with enough size std::uint16_t port = 0; @@ -41,6 +46,8 @@ namespace CppSockets { ss >> port; } + // TODO: Error handling + return std::make_shared(ipv4, port); } } diff --git a/source/Socket.cpp b/source/Socket.cpp index b47e491..e9cf847 100644 --- a/source/Socket.cpp +++ b/source/Socket.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sat Jan 15 01:27:40 2022 Francois Michaut -** Last update Mon Aug 10 22:11:59 2026 Francois Michaut +** Last update Sun Oct 25 13:27:58 2026 Francois Michaut ** ** Socket.cpp : Protable C++ socket class implementation */ @@ -16,6 +16,8 @@ #ifdef OS_WINDOWS #include #include + + #define SHUT_RDWR SD_BOTH #else #include #include @@ -25,9 +27,6 @@ // To match windows's constants static constexpr int INVALID_SOCKET = -1; static constexpr int SOCKET_ERROR = -1; - - #include - #include #endif static constexpr int BUFF_SIZE = 4096; @@ -35,6 +34,7 @@ static constexpr int BUFF_SIZE = 4096; #include #include #include +#include #include #include @@ -44,12 +44,39 @@ namespace CppSockets { Socket::Socket(RawSocketType sockfd, bool connected) : m_sockfd(sockfd), m_is_connected(connected) { - socklen_t len = sizeof(int); + Socket::getsockopt(sockfd, SOL_SOCKET, SO_TYPE, reinterpret_cast(&m_type), sizeof(int)); + +#ifdef SO_DOMAIN + // Fast path for Linux + Socket::getsockopt(sockfd, SOL_SOCKET, SO_DOMAIN, &m_domain, sizeof(int)); +#else + struct sockaddr_storage addr{}; + socklen_t len = sizeof(addr); + if (getsockname(sockfd, reinterpret_cast(&addr), &len) == -1) { + throw_errno_error("Failed to getsockname: "); + } + m_domain = addr.ss_family; +#endif + +#ifdef SO_PROTOCOL + // Fast path for Linux + Socket::getsockopt(sockfd, SOL_SOCKET, SO_PROTOCOL, &m_protocol, sizeof(int)); +#elif defined (OS_WINDOWS) + // Windows fallback using Protocol Info + WSAPROTOCOL_INFO info; + Socket::getsockopt(sockfd, SOL_SOCKET, SO_PROTOCOL_INFO, (char *)&info, sizeof(info)); - Socket::getsockopt(sockfd, SOL_SOCKET, SO_TYPE, reinterpret_cast(&m_type), &len); -#ifdef OS_LINUX - Socket::getsockopt(sockfd, SOL_SOCKET, SO_DOMAIN, &m_domain, &len); - Socket::getsockopt(sockfd, SOL_SOCKET, SO_PROTOCOL, &m_protocol, &len); + m_protocol = info.iProtocol; +#else + if (m_domain == AF_INET || m_domain == AF_INET6) { + if (m_type == SOCK_STREAM) { + m_protocol = IPPROTO_TCP; + } else if (m_type == SOCK_DGRAM) { + m_protocol = IPPROTO_UDP; + } else { + m_protocol = -1; + } + } #endif } @@ -60,15 +87,17 @@ namespace CppSockets { {} Socket::Socket(int domain, int type, int protocol) : - m_domain(domain), m_type(type), m_protocol(protocol), m_sockfd(::socket(domain, type, protocol)) + m_domain(domain), m_type(type), m_protocol(protocol), m_sockfd(INVALID_SOCKET) { - if (m_sockfd == INVALID_SOCKET) { - throw std::runtime_error(std::string("Failed to create socket : ") + std::strerror(errno)); - } + this->create_socket(); } Socket::~Socket() { - close(); + try { + close(); + } catch (const std::runtime_error &e) { + std::cerr << e.what() << '\n'; + } } Socket::Socket(Socket &&other) noexcept : @@ -79,59 +108,55 @@ namespace CppSockets { auto Socket::operator=(Socket &&other) noexcept -> Socket & { std::swap(m_sockfd, other.m_sockfd); - - m_domain = other.m_domain; - m_is_connected = other.m_is_connected; + std::swap(m_domain, other.m_domain); + std::swap(m_type, other.m_type); + std::swap(m_protocol, other.m_protocol); + std::swap(m_is_connected, other.m_is_connected); return *this; } - void Socket::close() { // NOLINT(readability-make-member-function-const) - if (m_sockfd != INVALID_SOCKET) { -#ifdef OS_WINDOWS - closesocket(m_sockfd); -#else - ::close(m_sockfd); -#endif + void Socket::close() { + if (m_is_connected && m_type == SOCK_STREAM) { + if (shutdown(m_sockfd, SHUT_RDWR) != 0) { + // log error instead of raising, errors shouldn't occur and if + // they do we still need to close() the FD. + std::cerr << "Failed to shutdown connection: " << strerror() << '\n'; + } + m_is_connected = false; } - } - - auto Socket::getsockopt(int fd, int level, int optname, SockOptType *optval, socklen_t *optlen) -> int { - int ret = ::getsockopt(fd, level, optname, optval, optlen); - - if (ret == SOCKET_ERROR) - throw std::runtime_error(std::string("Failed to get sock opt: ") + Socket::strerror()); - return ret; - } - - auto Socket::strerror() -> char * { - return Socket::strerror(Socket::get_errno()); - } - - auto Socket::strerror(int err) -> char * { - return ::strerror(err); - } - auto Socket::get_errno() -> int { + if (m_sockfd != INVALID_SOCKET) { #ifdef OS_WINDOWS - return WSAGetLastError(); + int res = closesocket(m_sockfd); #else - return errno; + int res = ::close(m_sockfd); #endif + if (res != 0) { + throw_errno_error("Failed to close socket: "); + } + + m_sockfd = INVALID_SOCKET; + } } auto Socket::read(std::size_t len) -> std::string { - std::array buff = {0}; // TODO: Avoid deallocation/reallocation everytime ? - std::stringstream res; - std::size_t total = 0; + std::array buff; + std::size_t buff_size = buff.size(); + std::string res; std::size_t nb = 1; - while (nb != 0 && (len == -1 || total < len)) { - nb = this->read(buff.data(), buff.size()); + if (len != -1) res.reserve(len); + + while (nb != 0 && len != 0) { + nb = this->read(buff.data(), (buff_size > len ? len : buff_size)); if (nb > 0) { - res << std::string(buff.data(), nb); + res.append(buff.data(), nb); + if (len != -1) { + len -= nb; + } } } - return res.str(); + return res; } auto Socket::read(char *buff, std::size_t size) -> std::size_t { @@ -141,14 +166,14 @@ namespace CppSockets { throw std::runtime_error("Not connected"); ret = ::read(m_sockfd, buff, size); if (ret < 0) - throw std::runtime_error(std::string("Failed to read from socket: ") + Socket::strerror()); + throw_errno_error("Failed to read from socket: "); if (ret == 0 && size > 0) { m_is_connected = false; } return ret; } - auto Socket::write(const std::string &buff) -> std::size_t { + auto Socket::write(std::string_view buff) -> std::size_t { return this->write(buff.data(), buff.size()); } @@ -159,7 +184,7 @@ namespace CppSockets { throw std::runtime_error("Not connected"); ret = ::write(m_sockfd, buff, len); if (ret < 0) - throw std::runtime_error(std::string("Failed to write to socket: ") + Socket::strerror()); + throw_errno_error("Failed to write to socket: "); return ret; } @@ -177,7 +202,7 @@ namespace CppSockets { int ret = ::setsockopt(m_sockfd, level, optname, optval, optlen); if (ret < 0) - throw std::runtime_error(std::string("Failed to set sock opt: ") + Socket::strerror()); + throw_errno_error("Failed to set socket option: "); return ret; } @@ -191,20 +216,6 @@ namespace CppSockets { return this->bind(endpoint.get_addr().get_address(), endpoint.get_port()); } - auto Socket::bind(std::uint32_t source_addr, uint16_t port) -> int { // NOLINT(readability-make-member-function-const) - struct sockaddr_in addr = {}; - int ret = 0; - - addr.sin_family = m_domain; - addr.sin_addr.s_addr = source_addr; - addr.sin_port = htons(port); - ret = ::bind(m_sockfd, reinterpret_cast(&addr), sizeof(addr)); - - if (ret < 0) - throw std::runtime_error(std::string("Failed to bind socket: ") + Socket::strerror()); - return ret; - } - auto Socket::connect(const std::string &addr, uint16_t port) -> int { return this->connect(Endpoint(IPv4(addr.c_str()), port)); } @@ -216,12 +227,16 @@ namespace CppSockets { addr.sin_addr.s_addr = endpoint.get_addr().get_address(); addr.sin_port = htons(endpoint.get_port()); addr.sin_family = endpoint.get_addr().get_family(); - // TODO: If connected close / reconnect + if (m_is_connected) { + this->close(); + } + if (m_sockfd == INVALID_SOCKET) { + this->create_socket(); + } ret = ::connect(m_sockfd, reinterpret_cast(&addr), sizeof(addr)); if (ret < 0) { this->close(); - m_sockfd = ::socket(m_domain, m_type, m_protocol); - throw std::runtime_error(std::string("Failed to connect socket to ") + endpoint.to_string() + " : " + Socket::strerror()); + throw_errno_error(std::string("Failed to connect socket to ") + endpoint.to_string() + " : "); } else { m_is_connected = true; } @@ -232,19 +247,29 @@ namespace CppSockets { int ret = ::listen(m_sockfd, backlog); if (ret < 0) - throw std::runtime_error(std::string("Failed to listen socket: ") + Socket::strerror()); + throw_errno_error("Failed to listen socket: "); return ret; } - auto Socket::accept(void *addr_out) -> std::unique_ptr { // NOLINT(readability-make-member-function-const) + auto Socket::accept(void *addr_out) -> std::unique_ptr { + return std::unique_ptr(acceptImpl(addr_out)); + } + + auto Socket::acceptImpl(void *addr_out) -> Socket * { // NOLINT(readability-make-member-function-const) int fd = ::accept(m_sockfd, nullptr, nullptr); if (addr_out != nullptr) { // TODO figure it out } - if (fd == INVALID_SOCKET) - return nullptr; - return std::make_unique(fd, true); + if (fd == INVALID_SOCKET) { + int err = this->get_errno(); + + if (err == EAGAIN || err == EWOULDBLOCK || err == EINTR) { + return nullptr; + } + throw_errno_error("Failed to accept: ", err); + } + return new Socket(fd, true); } void Socket::set_blocking(bool val) { // NOLINT(readability-make-member-function-const) @@ -252,7 +277,7 @@ namespace CppSockets { u_long mode = val ? 0 : 1; int result = ioctlsocket(m_sockfd, FIONBIO, &mode); if (result != NO_ERROR) { - throw std::runtime_error(std::string("Failed to change socket: ") + Socket::strerror()); + throw_errno_error("Failed to change socket blocking mode: "); } #else int flags = fcntl(m_sockfd, F_GETFL, 0); @@ -260,14 +285,80 @@ namespace CppSockets { if (ret >= 0) { if (val) { - ret = fcntl(m_sockfd, F_SETFL, flags | O_NONBLOCK); // NOLINT(cppcoreguidelines-pro-type-vararg, hicpp-vararg, hicpp-signed-bitwise) + ret = fcntl(m_sockfd, F_SETFL, flags & ~O_NONBLOCK); // NOLINT(cppcoreguidelines-pro-type-vararg, hicpp-vararg, hicpp-signed-bitwise) } else { - ret = fcntl(m_sockfd, F_SETFL, (flags | O_NONBLOCK) ^ O_NONBLOCK); // NOLINT(cppcoreguidelines-pro-type-vararg, hicpp-vararg, hicpp-signed-bitwise) + ret = fcntl(m_sockfd, F_SETFL, flags | O_NONBLOCK); // NOLINT(cppcoreguidelines-pro-type-vararg, hicpp-vararg, hicpp-signed-bitwise) } } if (ret < 0) { - throw std::runtime_error(std::string("Failed to change socket: ") + Socket::strerror()); + throw_errno_error("Failed to change socket blocking mode: "); } #endif } + + auto Socket::getsockopt(int fd, int level, int optname, SockOptType *optval, socklen_t *optlen) -> int { + int ret = ::getsockopt(fd, level, optname, optval, optlen); + + if (ret == SOCKET_ERROR) + throw_errno_error("Failed to get sock opt: "); + return ret; + } + + auto Socket::bind(std::uint32_t source_addr, uint16_t port) -> int { // NOLINT(readability-make-member-function-const) + struct sockaddr_in addr = {}; + int ret = 0; + + addr.sin_family = m_domain; + addr.sin_addr.s_addr = source_addr; + addr.sin_port = htons(port); + ret = ::bind(m_sockfd, reinterpret_cast(&addr), sizeof(addr)); + + if (ret < 0) + throw_errno_error("Failed to bind socket: "); + return ret; + } + + auto Socket::strerror() -> std::string { + return Socket::strerror(Socket::get_errno()); + } + + auto Socket::strerror(int err) -> std::string { +#ifdef OS_WINDOWS + char *msg = nullptr; + + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, err, 0, (LPSTR)&msg, 0, nullptr); + std::string result(msg ? msg : "Unknown error"); + LocalFree(msg); + return result; +#else + std::array buf{}; + strerror_r(err, buf.data(), buf.size()); + return buf.data(); +#endif + } + + auto Socket::get_errno() -> int { +#ifdef OS_WINDOWS + return WSAGetLastError(); +#else + return errno; +#endif + } + + void Socket::throw_errno_error(std::string message) { + Socket::throw_errno_error(std::move(message), Socket::get_errno()); + } + + void Socket::throw_errno_error(std::string message, int err) { + throw std::runtime_error(std::move(message) + Socket::strerror(err)); + } + + void Socket::create_socket() { + m_sockfd = ::socket(m_domain, m_type, m_protocol); + if (m_sockfd == INVALID_SOCKET) { + throw_errno_error("Failed to create socket : "); + } + } } diff --git a/source/SocketInit.cpp b/source/SocketInit.cpp index 2b9cef9..498565f 100644 --- a/source/SocketInit.cpp +++ b/source/SocketInit.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Thu Sep 15 14:24:25 2022 Francois Michaut -** Last update Mon Aug 10 22:38:13 2026 Francois Michaut +** Last update Thu Oct 22 22:06:42 2026 Francois Michaut ** ** init.cpp : Startup/Cleanup functions implementation */ diff --git a/source/Tls/Callbacks.cpp b/source/Tls/Callbacks.cpp new file mode 100644 index 0000000..d8d9c44 --- /dev/null +++ b/source/Tls/Callbacks.cpp @@ -0,0 +1,74 @@ +/* +** Project LibCppSockets, 2026 +** +** Author Francois Michaut +** +** Started on Sat Oct 24 23:24:43 2026 Francois Michaut +** Last update Sun Oct 25 12:15:48 2026 Francois Michaut +** +** Callbacks.cpp : Centralization of the boilerplate for verify callbacks +*/ + +#include "CppSockets/Tls/Context.hpp" +#include "CppSockets/Tls/Socket.hpp" + +namespace { + struct TlsCallbackData { + CppSockets::TlsVerifyCallback callback; + }; + + void tls_context_ex_data_free(void *_parent, void *ptr, CRYPTO_EX_DATA *_ad, int _idx, long _argl, void *_argp) { + delete static_cast(ptr); + } + + // TODO: Free this ? + // NOLINTNEXTLINE(cert-err58-cpp) + const int TLS_CONTEXT_CALLBACK_STRUCT_IDX = SSL_CTX_get_ex_new_index(0, (void *)"TlsContext callback struct index", nullptr, nullptr, tls_context_ex_data_free); + // NOLINTNEXTLINE(cert-err58-cpp) + const int TLS_SOCKET_CALLBACK_STRUCT_IDX = SSL_get_ex_new_index(0, (void *)"TlsSocket callback struct index", nullptr, nullptr, tls_context_ex_data_free); + + auto tls_context_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) -> int { + SSL *ssl = static_cast(X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); + SSL_CTX *ssl_ctx = SSL_get_SSL_CTX(ssl); + + auto *data = static_cast(SSL_CTX_get_ex_data(ssl_ctx, TLS_CONTEXT_CALLBACK_STRUCT_IDX)); + return data->callback(preverify_ok, ctx); + } + + auto ssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) -> int { + SSL *ssl = static_cast(X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); + + auto *data = static_cast(SSL_get_ex_data(ssl, TLS_SOCKET_CALLBACK_STRUCT_IDX)); + return data->callback(preverify_ok, ctx); + } +} + +namespace CppSockets { + void TlsContext::set_verify(int mode, TlsVerifyCallback callback) { + void *previous = SSL_CTX_get_ex_data(m_ptr.get(), TLS_CONTEXT_CALLBACK_STRUCT_IDX); + + delete static_cast(previous); + if (callback) { + auto *cb_data = new TlsCallbackData{std::move(callback)}; + + SSL_CTX_set_ex_data(m_ptr.get(), TLS_CONTEXT_CALLBACK_STRUCT_IDX, cb_data); + SSL_CTX_set_verify(m_ptr.get(), mode, tls_context_verify_callback); + } else { + SSL_CTX_set_verify(m_ptr.get(), mode, nullptr); + } + } + + void TlsSocket::set_verify(int mode, TlsVerifyCallback callback) { + void *previous = SSL_get_ex_data(m_ssl.get(), TLS_SOCKET_CALLBACK_STRUCT_IDX); + + delete static_cast(previous); + if (callback) { + auto *cb_data = new TlsCallbackData{std::move(callback)}; + + SSL_set_ex_data(m_ssl.get(), TLS_SOCKET_CALLBACK_STRUCT_IDX, cb_data); + SSL_set_verify(m_ssl.get(), mode, ssl_verify_callback); + } else { + SSL_set_verify(m_ssl.get(), mode, nullptr); + } + } +} diff --git a/source/Tls/Context.cpp b/source/Tls/Context.cpp index dae0432..45fac4e 100644 --- a/source/Tls/Context.cpp +++ b/source/Tls/Context.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Aug 20 14:40:41 2025 Francois Michaut -** Last update Tue Jun 30 08:38:15 2026 Francois Michaut +** Last update Sun Oct 25 00:18:04 2026 Francois Michaut ** ** Context.cpp : Implementation of the Context for TLS sockets */ @@ -13,36 +13,15 @@ #include "CppSockets/SslMacros.hpp" +#include #include #include -#include #include -namespace CppSockets { - // TODO: Free this ? - const int TLS_CONTEXT_IDX = SSL_CTX_get_ex_new_index(0, (void *)"TlsContent index", nullptr, nullptr, nullptr); - - struct TlsContext_Accessor { - static auto get_function(TlsContext &ctx) -> TlsVerifyCallback & { - return ctx.m_verify_callback; - } - }; - - static auto tls_context_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) -> int { - SSL *ssl = static_cast(X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); - SSL_CTX *ssl_ctx = SSL_get_SSL_CTX(ssl); - - auto *tls_ctx = static_cast(SSL_CTX_get_ex_data(ssl_ctx, TLS_CONTEXT_IDX)); - - return CppSockets::TlsContext_Accessor::get_function(*tls_ctx)(preverify_ok, ctx); - } -} - #define TLS_CONTEXT_CONSTRUCTOR_BODY \ REQUIRED_PTR(m_ptr, "SSL_CTX"); \ - SSL_CTX_set_ex_data(m_ptr.get(), TLS_CONTEXT_IDX, this); \ - set_min_proto_version(TLS1_1_VERSION); + set_min_proto_version(TLS1_2_VERSION); namespace CppSockets { TlsContext::TlsContext() : @@ -63,20 +42,22 @@ namespace CppSockets { TLS_CONTEXT_CONSTRUCTOR_BODY; } - TlsContext::TlsContext(TlsContext &&other) noexcept { + TlsContext::TlsContext(const TlsContext &other) { *this = other; } + TlsContext::TlsContext(TlsContext &&other) noexcept { + *this = std::move(other); + } + auto TlsContext::operator=(const TlsContext &other) -> TlsContext & { UP_REF_ASSIGNMENT_OPERATOR(SSL_CTX); - m_verify_callback = other.m_verify_callback; } auto TlsContext::operator=(TlsContext &&other) noexcept -> TlsContext & { std::swap(m_ptr, other.m_ptr); std::swap(m_own, other.m_own); - m_verify_callback = std::move(other.m_verify_callback); return *this; } @@ -88,18 +69,6 @@ namespace CppSockets { } } - void TlsContext::set_verify(int mode, TlsVerifyCallback callback) { - if (!m_own) { - // We would loose the TlsVerifyCallback if the TlsContext goes out of scope, but the SSL_CTX - // hasn't been freed yet, which would result in a crash if the callback was then requested - throw std::runtime_error("Can't set_verify on a non-owned TlsContext. Use the Raw SSL_CTX methods"); - } - m_verify_callback = std::move(callback); - SSL_verify_cb raw_cb = m_verify_callback ? tls_context_verify_callback : nullptr; - - SSL_CTX_set_verify(m_ptr.get(), mode, raw_cb); - } - void TlsContext::set_verify_depth(int depth) { SSL_CTX_set_verify_depth(m_ptr.get(), depth); } diff --git a/source/Tls/Socket.cpp b/source/Tls/Socket.cpp index 3c380c3..07a7523 100644 --- a/source/Tls/Socket.cpp +++ b/source/Tls/Socket.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Sep 14 21:04:42 2022 Francois Michaut -** Last update Tue Aug 11 00:26:38 2026 Francois Michaut +** Last update Sun Oct 25 12:30:49 2026 Francois Michaut ** ** SecureSocket.cpp : TLS socket wrapper implementation */ @@ -18,12 +18,13 @@ #include #include +#include #include #include static constexpr int BUFF_SIZE = 4096; -namespace { +namespace { auto extract_errors_from_queue() -> std::string { auto error = ERR_get_error(); std::stringstream ss; @@ -39,10 +40,13 @@ namespace { } void init_ssl_socket(SSL *ssl, CppSockets::TlsSocket *socket) { - if (!ssl || !SSL_set_fd(ssl, socket->get_fd())) { + if (!ssl) { + throw std::runtime_error("Failed to initialize TLS socket: " + extract_errors_from_queue()); + } + if (!SSL_set_fd(ssl, socket->get_fd())) { throw std::runtime_error(std::string("Failed to initialize TLS socket: ") + socket->tls_strerror(0)); } - if (!SSL_set_min_proto_version(ssl, TLS1_VERSION)) { + if (!SSL_set_min_proto_version(ssl, TLS1_2_VERSION)) { throw std::runtime_error(std::string("Failed to select TLS version: ") + socket->tls_strerror(0)); } } @@ -59,6 +63,12 @@ namespace CppSockets { init_ssl_socket(m_ssl.get(), this); } + TlsSocket::TlsSocket(TlsSocket &&other) noexcept : + Socket(std::move(other)), m_ctx(std::move(other.m_ctx)), + m_ssl(std::move(other.m_ssl)), m_peer_cert(std::move(other.m_peer_cert)), + m_should_shutdown(other.m_should_shutdown) + {} + TlsSocket::TlsSocket(Socket &&other, TlsContext ctx) : CppSockets::Socket(std::move(other)), // TODO: if socket is not connected, at that moment, does it break ? m_ctx(std::move(ctx)), m_ssl(SSL_ptr(SSL_new(m_ctx.get()))), m_peer_cert(nullptr) @@ -76,52 +86,44 @@ namespace CppSockets { } TlsSocket::~TlsSocket() noexcept { - if (m_ssl && this->connected()) { - // TODO: Better shutdown mecanics - int ret = SSL_shutdown(m_ssl.get()); - - // if (ret == 1) { - // // Peer also closed -> We can leave. - // } else if (ret == 0) { - // // Peer didn't send, but we can't wait in the Destructor - // } else { - // // TODO: log failure - // } + try { + this->close(true); + } catch (const std::runtime_error& e) { + std::cerr << "Failed to close TlsSocket: " << e.what() << '\n'; } } - TlsSocket::TlsSocket(TlsSocket &&other) noexcept { - *this = std::move(other); - } - auto TlsSocket::operator=(TlsSocket &&other) noexcept -> TlsSocket & { m_ssl = std::move(other.m_ssl); m_ctx = std::move(other.m_ctx); m_peer_cert = std::move(other.m_peer_cert); + std::swap(m_should_shutdown, other.m_should_shutdown); + Socket::operator=(std::move(other)); return *this; } - void TlsSocket::close() { - if (m_ssl && this->connected()) { + bool TlsSocket::close(bool force) { + if (m_ssl && m_should_shutdown) { int ret = SSL_shutdown(m_ssl.get()); - if (ret == 1) { - return Socket::close(); + if (ret < 0) { + throw_tls_error("Failed to shutdown TLS: ", ret); + } else if (ret == 1 || force) { + Socket::close(); + } else { // ret == 0 + // TODO: wait for peer + return false; } - // if (ret == 0) { - // // TODO: wait for peer - // } else { - // // TODO: Log failure - // } + } else if (this->connected()) { + Socket::close(); } + return true; } - void TlsSocket::set_verify(int mode, SSL_verify_cb verify_callback) { - // TODO: While setting it on the CTX makes sense imo (since accepted sockets will inherit this), an application - // might not want that behavior. Need to provide alertnate ways to set verify on CTX vs SSL - SSL_CTX_set_verify(m_ctx.get(), mode, verify_callback); + void TlsSocket::set_verify_depth(int depth) { + SSL_set_verify_depth(m_ssl.get(), depth); } void TlsSocket::set_certificate(const std::string &cert_path, const std::string &pkey_path) { @@ -132,34 +134,38 @@ namespace CppSockets { EVP_PKEY_ptr evp_pkey(PEM_read_bio_PrivateKey(pkey.get(), nullptr, nullptr, nullptr)); if (SSL_use_certificate(m_ssl.get(), x509.get()) <= 0) { - throw std::runtime_error(std::string("Failed to set certificate: ") + TlsSocket::tls_strerror(0)); + throw_tls_error("Failed to set certificate: "); } if (SSL_use_PrivateKey(m_ssl.get(), evp_pkey.get()) <= 0 ) { - throw std::runtime_error(std::string("Failed to set private key: ") + TlsSocket::tls_strerror(0)); + throw_tls_error("Failed to set private key: "); } } // TODO add SSL_get_shutdown checks in read operations auto TlsSocket::read(std::size_t len) -> std::string { - std::array buff = {0}; - std::stringstream res; - std::size_t nb = 0; + std::array buff; + std::size_t buff_size = buff.size(); + std::string res; + std::size_t nb = 1; std::size_t total; + if (len != -1) res.reserve(len); + if (SSL_peek(m_ssl.get(), buff.data(), buff.size()) <= 0) { - set_connected(false); // TODO: we should replace this with check_for_error + m_should_shutdown = false; // TODO: we should replace this with check_for_error } check_for_error("Failed to read from socket", 1); // Do not raise an error if peek failed total = SSL_pending(m_ssl.get()); - while (total != 0 && len != 0) { - nb = this->read(buff.data(), (buff.size() > len ? len : buff.size())); - res << std::string(buff.data(), nb); + while (nb != 0 && total != 0 && len != 0) { + nb = this->read(buff.data(), (buff_size > len ? len : buff_size)); + res.append(buff.data(), nb); total -= nb; - if (len != -1) + if (len != -1) { len -= nb; + } } - return res.str(); + return res; } auto TlsSocket::read(char *buff, std::size_t size) -> std::size_t { @@ -178,16 +184,16 @@ namespace CppSockets { return nb; } - void TlsSocket::check_for_error(const std::string &error_msg, int ret) { + void TlsSocket::check_for_error(std::string error_msg, int ret) { int shutdown = SSL_get_shutdown(m_ssl.get()); if (shutdown == SSL_RECEIVED_SHUTDOWN) { SSL_shutdown(m_ssl.get()); // TODO: log failure - set_connected(false); + m_should_shutdown = false; } if (ret <= 0) { - throw std::runtime_error(error_msg + TlsSocket::tls_strerror(ret)); + throw_tls_error(std::move(error_msg), ret); } } @@ -196,14 +202,19 @@ namespace CppSockets { int ssl_ret = SSL_connect(m_ssl.get()); if (ssl_ret != 1) { - throw std::runtime_error(std::string("Failed to connect: ") + TlsSocket::tls_strerror(ssl_ret)); + throw_tls_error("Failed to connect: ", ssl_ret); } + m_should_shutdown = true; m_peer_cert.reset(SSL_get_peer_certificate(m_ssl.get())); return ret; } auto TlsSocket::accept(void *addr_out, TlsContext ctx) -> std::unique_ptr { - std::unique_ptr res = Socket::accept(addr_out); + return std::unique_ptr(this->acceptImpl(addr_out, ctx)); + } + + auto TlsSocket::acceptImpl(void *addr_out, TlsContext ctx) -> TlsSocket * { + std::unique_ptr res{Socket::acceptImpl(addr_out)}; std::unique_ptr tls; int ssl_ret = 0; @@ -214,10 +225,11 @@ namespace CppSockets { tls = std::make_unique(std::move(*res), std::move(ctx)); ssl_ret = SSL_accept(tls->get_ssl().get()); if (ssl_ret <= 0) { - throw std::runtime_error("Failed to accept TLS connection: " + TlsSocket::tls_strerror(ssl_ret)); + throw_tls_error("Failed to accept TLS connection: ", ssl_ret); } + tls->m_should_shutdown = true; tls->m_peer_cert.reset(SSL_get_peer_certificate(tls->get_ssl().get())); - return tls; + return tls.release(); } auto TlsSocket::get_ssl_ctx() const -> TlsContext { @@ -225,6 +237,8 @@ namespace CppSockets { } auto TlsSocket::tls_strerror(int ret) -> std::string { + if (!m_ssl) return "SSL object is null"; + int err = SSL_get_error(m_ssl.get(), ret); switch (err) { @@ -249,13 +263,21 @@ namespace CppSockets { case SSL_ERROR_WANT_CLIENT_HELLO_CB: return "Retry the operation later: WANT_CLIENT_HELLO_CB"; case SSL_ERROR_SYSCALL: - set_connected(false); + m_should_shutdown = false; return std::string("Fatal system error: ") + Socket::strerror() + '\n' + extract_errors_from_queue(); case SSL_ERROR_SSL: - set_connected(false); + m_should_shutdown = false; return "Fatal TLS error: " + extract_errors_from_queue(); default: return "Unknown error: " + std::to_string(err) + '\n' + extract_errors_from_queue(); } } + + void TlsSocket::throw_tls_error(std::string message) { + throw_tls_error(std::move(message), 0); + } + + void TlsSocket::throw_tls_error(std::string message, int ret) { + throw std::runtime_error(std::move(message) + tls_strerror(ret)); + } }