From 8355589b190b3b73f2f85a526b13ff85d5310148 Mon Sep 17 00:00:00 2001 From: Francois Michaut Date: Sun, 25 Oct 2026 19:13:23 -0400 Subject: [PATCH] More bugfixes & improvements - Removed ambiguity around IPv4 byte order - Socket::accept now supports out addr param - TlsSocket now supports setting peer hostname - Fixed Windows error handling with new unified constants - Improved IEndpoint parsing - Clearly document that IPv6 isn't yet supported with specific errors - Better socket re-use / reconnect (specially for TlsSocket) - Improve non-blocking socket support (including TlsSocket::accept) - Properly fix TlsCallbackData cleanup - dup const Tls objects and take ownership for better const correctness - Safer security defaults on TlsContext --- .clang-tidy | 4 +- .clangd | 5 + CMakeLists.txt | 8 +- include/CppSockets/Address.hpp | 4 +- include/CppSockets/ErrorUtils.hpp | 20 ++ include/CppSockets/IPv4.hpp | 15 +- include/CppSockets/OSDetection.hpp | 2 +- include/CppSockets/PollEvent.hpp | 61 +++++ include/CppSockets/Socket.hpp | 31 ++- include/CppSockets/Tls/Certificate.hpp | 2 +- include/CppSockets/Tls/Context.hpp | 2 + include/CppSockets/Tls/Socket.hpp | 30 ++- include/CppSockets/Tls/Utils.hpp | 7 +- include/CppSockets/Version.hpp.in | 2 +- include/CppSockets/internal/DLLExport.hpp | 2 +- include/CppSockets/internal/SocketInit.hpp | 2 +- private/CppSockets/SocketInternals.hpp | 44 ++++ source/Address.cpp | 33 ++- source/ErrorUtils.cpp | 72 ++++++ source/IPv4.cpp | 20 +- source/Socket.cpp | 216 +++++++++++------- source/SocketInit.cpp | 13 +- source/Tls/Callbacks.cpp | 8 +- source/Tls/Certificate.cpp | 23 +- source/Tls/Context.cpp | 21 +- source/Tls/Socket.cpp | 251 +++++++++++---------- source/Tls/Utils.cpp | 59 ++++- tests/CMakeLists.txt | 2 +- tests/TestSockets.cpp | 4 +- tests/Tls/TestContext.cpp | 2 +- tests/Tls/TestOpensslWrappers.cpp | 2 +- 31 files changed, 677 insertions(+), 290 deletions(-) create mode 100644 .clangd create mode 100644 include/CppSockets/ErrorUtils.hpp create mode 100644 include/CppSockets/PollEvent.hpp create mode 100644 private/CppSockets/SocketInternals.hpp create mode 100644 source/ErrorUtils.cpp diff --git a/.clang-tidy b/.clang-tidy index 4bb3b92..5f474c4 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -6,6 +6,7 @@ Checks: ' -fuchsia*, -google-readability-todo, -cppcoreguidelines-init-variables, +-cppcoreguidelines-avoid-do-while, -cppcoreguidelines-owning-memory, -cppcoreguidelines-pro-type-reinterpret-cast, -llvm-namespace-comment, @@ -15,6 +16,7 @@ Checks: ' -readability-avoid-return-with-void-value, -readability-convert-member-functions-to-static, +-misc-use-anonymous-namespace, -google-readability-braces-around-statements, -google-readability-namespace-comments, -hicpp-special-member-functions, @@ -27,7 +29,7 @@ FormatStyle: 'none' # TODO: Replace with 'file' once we have a proper .clang-for InheritParentConfig: true CheckOptions: misc-include-cleaner.MissingIncludes: 'false' - misc-include-cleaner.IgnoreHeaders: 'CppSockets/OSDetection.*' + misc-include-cleaner.IgnoreHeaders: 'CppSockets/OSDetection\.hpp;CppSockets/SocketInternals\.hpp' bugprone-argument-comment.StrictMode: 1 diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..f70eec8 --- /dev/null +++ b/.clangd @@ -0,0 +1,5 @@ +Diagnostics: + Includes: + IgnoreHeader: + - "CppSockets/OSDetection\\.hpp" + - "CppSockets/SocketInternals\\.hpp" diff --git a/CMakeLists.txt b/CMakeLists.txt index 201343e..28052f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,12 +4,12 @@ ## Author Francois Michaut ## ## Started on Sun Aug 28 19:26:51 2022 Francois Michaut -## Last update Sat Oct 24 23:32:08 2026 Francois Michaut +## Last update Mon Oct 26 22:43:14 2026 Francois Michaut ## ## CMakeLists.txt : CMake to build the CppSockets library ## -cmake_minimum_required (VERSION 3.15) +cmake_minimum_required (VERSION 3.19) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -28,6 +28,7 @@ add_library(cppsockets source/Tls/Socket.cpp source/Address.cpp + source/ErrorUtils.cpp source/IPv4.cpp source/Socket.cpp source/SocketInit.cpp @@ -42,7 +43,7 @@ target_include_directories(cppsockets ) if(WIN32) - target_compile_definitions(cppsockets PRIVATE CPPSOCKETS_DLL_EXPORTS=1) + target_compile_definitions(cppsockets PRIVATE CPPSOCKETS_DLL_EXPORTS=1 NOMINMAX) endif() target_compile_options(cppsockets PRIVATE @@ -58,5 +59,6 @@ endif() option(CPPSOCKETS_TESTS "TRUE to build the libcppsockets tests" FALSE) if(CPPSOCKETS_TESTS) + include(CTest) add_subdirectory(tests) endif() diff --git a/include/CppSockets/Address.hpp b/include/CppSockets/Address.hpp index 4b77af4..c33d18d 100644 --- a/include/CppSockets/Address.hpp +++ b/include/CppSockets/Address.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sun Feb 13 17:09:05 2022 Francois Michaut -** Last update Wed Jul 1 14:19:51 2026 Francois Michaut +** Last update Mon Oct 26 12:13:43 2026 Francois Michaut ** ** Address.hpp : Interface to represent network addresses */ @@ -36,9 +36,7 @@ namespace CppSockets { [[nodiscard]] virtual auto get_addr() const -> const IAddress & = 0; [[nodiscard]] virtual auto to_string() const -> const std::string & = 0; - static auto from_string(std::string endpoint) -> std::shared_ptr; static auto from_string(std::string_view endpoint) -> std::shared_ptr; - protected: [[nodiscard]] auto make_string() const -> std::string; }; diff --git a/include/CppSockets/ErrorUtils.hpp b/include/CppSockets/ErrorUtils.hpp new file mode 100644 index 0000000..1cb1437 --- /dev/null +++ b/include/CppSockets/ErrorUtils.hpp @@ -0,0 +1,20 @@ +/* +** Project LibCppSockets, 2026 +** +** Author Francois Michaut +** +** Started on Mon Oct 26 22:24:25 2026 Francois Michaut +** Last update Mon Oct 26 22:44:34 2026 Francois Michaut +** +** ErrorUtils.hpp : Portable system error utilities +*/ + +#pragma once + +#include + +namespace CppSockets { + auto get_errno() -> int; + auto strerror(int err) -> std::string; + auto strerror() -> std::string; +} diff --git a/include/CppSockets/IPv4.hpp b/include/CppSockets/IPv4.hpp index 0a05349..80d15de 100644 --- a/include/CppSockets/IPv4.hpp +++ b/include/CppSockets/IPv4.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sun Feb 13 17:05:02 2022 Francois Michaut -** Last update Wed Jul 1 14:19:39 2026 Francois Michaut +** Last update Mon Oct 26 14:26:38 2026 Francois Michaut ** ** IPv4.hpp : Class used to represent and manipulate IPv4 addresses */ @@ -13,18 +13,25 @@ #include "CppSockets/Address.hpp" +#include + namespace CppSockets { class IPv4 : public IAddress { public: - explicit IPv4(std::uint32_t addr); - IPv4(const char *addr); // TODO add support for string. Maybe string_view ? + IPv4(std::string_view addr); + + static auto from_host_order(std::uint32_t addr) -> IPv4; + static auto from_network_order(std::uint32_t addr) -> IPv4; + // returns address in network byte order [[nodiscard]] auto get_address() const -> std::uint32_t override; [[nodiscard]] auto get_family() const -> int override; [[nodiscard]] auto to_string() const -> const std::string & override; private: - std::uint32_t addr; + IPv4(std::uint32_t addr); + + std::uint32_t addr; // always network byte order std::string str; }; using EndpointV4 = Endpoint; diff --git a/include/CppSockets/OSDetection.hpp b/include/CppSockets/OSDetection.hpp index 4b36951..6fdff1b 100644 --- a/include/CppSockets/OSDetection.hpp +++ b/include/CppSockets/OSDetection.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Tue May 9 23:20:20 2023 Francois Michaut -** Last update Wed Jul 1 14:20:01 2026 Francois Michaut +** Last update Sun Oct 25 23:15:10 2026 Francois Michaut ** ** OSDetection.hpp : OS Detection macros */ diff --git a/include/CppSockets/PollEvent.hpp b/include/CppSockets/PollEvent.hpp new file mode 100644 index 0000000..684129e --- /dev/null +++ b/include/CppSockets/PollEvent.hpp @@ -0,0 +1,61 @@ +/* +** Project LibCppSockets, 2022 +** +** Author Francois Michaut +** +** Started on Mon Oct 26 09:37:12 2026 Francois Michaut +** Last update Mon Oct 26 09:48:55 2026 Francois Michaut +** +** PollEvent.hpp : Helper enum for Socket::poll() +*/ + +#pragma once + +#include + +namespace CppSockets { + class PollEvent { + public: + enum Value : std::uint8_t { + NONE = 0, + READ = 1, + WRITE = 2, + }; + + constexpr PollEvent() : m_value(Value::NONE) {} + constexpr PollEvent(Value value) : m_value(value) {} + + constexpr auto operator|=(PollEvent other) -> PollEvent & { + m_value = static_cast(to_uint(m_value) | to_uint(other.m_value)); + return *this; + } + constexpr auto operator&=(PollEvent other) -> PollEvent & { + m_value = static_cast(to_uint(m_value) & to_uint(other.m_value)); + return *this; + } + [[nodiscard]] constexpr auto operator|(PollEvent other) const -> PollEvent { + other |= *this; + return other; + } + [[nodiscard]] constexpr auto operator&(PollEvent other) const -> PollEvent { + other &= *this; + return other; + } + [[nodiscard]] constexpr auto operator==(PollEvent other) const -> bool { return m_value == other.m_value; } + [[nodiscard]] constexpr auto operator!=(PollEvent other) const -> bool { return m_value != other.m_value; } + + explicit constexpr operator bool() const { return m_value != Value::NONE; } + + [[nodiscard]] constexpr auto has(PollEvent event) const -> bool { + return (to_uint(m_value) & to_uint(event.m_value)) != 0; + } + [[nodiscard]] constexpr auto value() const -> Value { return m_value; } + + private: + static constexpr auto to_uint(Value value) -> std::uint8_t { + return static_cast(value); + } + + Value m_value; + }; +} diff --git a/include/CppSockets/Socket.hpp b/include/CppSockets/Socket.hpp index 806cae1..6debe77 100644 --- a/include/CppSockets/Socket.hpp +++ b/include/CppSockets/Socket.hpp @@ -4,32 +4,35 @@ ** Author Francois Michaut ** ** Started on Sat Jan 15 01:17:42 2022 Francois Michaut -** Last update Sun Oct 25 11:38:13 2026 Francois Michaut +** Last update Mon Oct 26 20:22:18 2026 Francois Michaut ** ** Socket.hpp : Portable C++ socket class */ #pragma once +#include "CppSockets/Address.hpp" #include "CppSockets/OSDetection.hpp" -#include "CppSockets/internal/SocketInit.hpp" +#include "CppSockets/PollEvent.hpp" // TODO: move the RawSocketType in CppSockets namespace #ifdef OS_WINDOWS - #define NOMINMAX #include using RawSocketType=SOCKET; using socklen_t=int; using SockOptType=char; + using PollfdType=WSAPOLLFD; #else #include using RawSocketType=int; using SockOptType=void; + using PollfdType=struct pollfd; #endif +#include #include -#include "CppSockets/Address.hpp" +#include "CppSockets/internal/SocketInit.hpp" namespace CppSockets { class Socket : SocketInit { @@ -47,9 +50,12 @@ namespace CppSockets { virtual void close(); - virtual auto read(std::size_t len = -1) -> std::string; + // TODO: Add support for writev/WSASend and sendfile/WSATransmitPackets + + // read() with no lenght blocks once then returns all currently available data + 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; + auto write(std::string_view buff) -> std::size_t { return this->write(buff.data(), buff.size()); }; virtual auto write(const char *buff, std::size_t len) -> std::size_t; auto set_reuseaddr(bool value) -> int; @@ -62,7 +68,11 @@ namespace CppSockets { auto bind(const IEndpoint &endpoint) -> int; auto bind(const std::string &addr, uint16_t port) -> int; auto listen(int backlog) -> int; - auto accept(void *addr_out = nullptr) -> std::unique_ptr; + + auto accept() -> std::unique_ptr; + auto accept(sockaddr_storage &addr) -> std::unique_ptr; + + [[nodiscard]] auto poll(PollEvent events, int timeout_ms = 0) const -> PollEvent; void set_blocking(bool val); @@ -74,10 +84,6 @@ namespace CppSockets { [[nodiscard]] auto connected() const -> bool { return m_is_connected; } - static auto get_errno() -> int; - 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); @@ -91,7 +97,8 @@ namespace CppSockets { void create_socket(); - virtual auto acceptImpl(void *addr_out) -> Socket *; + [[nodiscard]] virtual auto has_more_data() const -> bool; + virtual auto acceptImpl(sockaddr_storage *addr_out) -> Socket *; private: int m_domain = 0; int m_type = 0; diff --git a/include/CppSockets/Tls/Certificate.hpp b/include/CppSockets/Tls/Certificate.hpp index aed44e1..05372e0 100644 --- a/include/CppSockets/Tls/Certificate.hpp +++ b/include/CppSockets/Tls/Certificate.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Fri Aug 1 09:50:33 2025 Francois Michaut -** Last update Thu Jul 2 01:03:49 2026 Francois Michaut +** Last update Sun Oct 25 23:16:01 2026 Francois Michaut ** ** Certificate.hpp : Classes to create and manage Certificates */ diff --git a/include/CppSockets/Tls/Context.hpp b/include/CppSockets/Tls/Context.hpp index bd8cd4a..ddb1a1d 100644 --- a/include/CppSockets/Tls/Context.hpp +++ b/include/CppSockets/Tls/Context.hpp @@ -30,6 +30,8 @@ namespace CppSockets { ~TlsContext(); void set_min_proto_version(int version); + void set_cipher_list(const char *cipher_list); + void set_ciphersuites(const char *ciphersuites); void set_verify(int mode, TlsVerifyCallback callback = {}); void set_verify_depth(int depth); diff --git a/include/CppSockets/Tls/Socket.hpp b/include/CppSockets/Tls/Socket.hpp index 1aadd97..034a14c 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 Sun Oct 25 12:00:59 2026 Francois Michaut +** Last update Mon Oct 26 21:37:23 2026 Francois Michaut ** ** SecureSocket.hpp : TLS socket wrapper using openssl */ @@ -35,29 +35,36 @@ namespace CppSockets { void close() override { this->close(false); }; bool close(bool force); - // 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; + // TODO: Add support for SSL_sendfile + SNI / ALPN 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, TlsVerifyCallback verify_callback = nullptr); void set_verify_depth(int depth); void set_certificate(const std::string &cert_path, const std::string &pkey_path); + // Must be called before connect() for hostname verification to take + // effect (requires peer verification to be enabled). + void set_hostname(const std::string &hostname) { set_hostname(hostname.c_str()); } + void set_hostname(const char *hostname); + + // TODO: Add connect overload that take a string hostname and port and call set_hostname automatically 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); } - auto accept(TlsContext ctx) -> std::unique_ptr { return accept(nullptr, std::move(ctx)); } + auto accept() -> std::unique_ptr { return accept(m_ctx); } + auto accept(TlsContext ctx) -> std::unique_ptr; + auto accept(sockaddr_storage &addr) -> std::unique_ptr { return accept(addr, m_ctx); } + auto accept(sockaddr_storage &addr, TlsContext ctx) -> std::unique_ptr; [[nodiscard]] auto get_ssl_ctx() const -> TlsContext; [[nodiscard]] auto get_ssl() const -> const SSL_ptr & { return m_ssl; } [[nodiscard]] auto get_peer_cert() const -> const X509_ptr & { return m_peer_cert; } - [[nodiscard]] auto tls_strerror(int ret) -> std::string; + [[nodiscard]] auto tls_errno(int ret) -> int; protected: - auto acceptImpl(void *addr_out) -> TlsSocket * override { return acceptImpl(addr_out, m_ctx); }; - auto acceptImpl(void *addr_out, TlsContext ctx) -> TlsSocket *; + auto acceptImpl(sockaddr_storage *addr_out) -> TlsSocket * override { return acceptImpl(addr_out, m_ctx); }; + auto acceptImpl(sockaddr_storage *addr_out, TlsContext ctx) -> TlsSocket *; + + [[nodiscard]] auto has_more_data() const -> bool override; void throw_tls_error(std::string message); void throw_tls_error(std::string message, int ret); @@ -67,6 +74,7 @@ namespace CppSockets { X509_ptr m_peer_cert; bool m_should_shutdown = false; - void check_for_error(std::string error_msg, int ret); + std::unique_ptr m_pending_accept; // partially handshaken accepted socket, for non-blocking sockets + auto handle_ssl_io_result(std::string error_msg, int ssl_ret, std::size_t nb) -> std::size_t; }; } diff --git a/include/CppSockets/Tls/Utils.hpp b/include/CppSockets/Tls/Utils.hpp index 94715ad..a28cf70 100644 --- a/include/CppSockets/Tls/Utils.hpp +++ b/include/CppSockets/Tls/Utils.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Fri Aug 1 09:54:53 2025 Francois Michaut -** Last update Wed Jul 1 19:48:56 2026 Francois Michaut +** Last update Mon Oct 26 22:32:36 2026 Francois Michaut ** ** Utils.hpp : Tls Utility types */ @@ -15,6 +15,7 @@ #include #include +#include #define CPP_SOCKETS_TLS_UTILS_DEFINE_DTOR(TYPE, PREFIX) \ struct TYPE##_dtor { \ @@ -53,6 +54,10 @@ namespace CppSockets { return ret; } + auto openssl_extract_errors_from_stack() -> std::string; + [[nodiscard]] auto ssl_strerror(int err) -> std::string; + + using TlsVerifyCallback=std::function; } diff --git a/include/CppSockets/Version.hpp.in b/include/CppSockets/Version.hpp.in index e4710df..e1bfb7c 100644 --- a/include/CppSockets/Version.hpp.in +++ b/include/CppSockets/Version.hpp.in @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sun Aug 28 19:32:30 2022 Francois Michaut -** Last update Wed Jul 1 14:19:47 2026 Francois Michaut +** Last update Sun Oct 25 23:14:59 2026 Francois Michaut ** ** Version.hpp.in : Library version */ diff --git a/include/CppSockets/internal/DLLExport.hpp b/include/CppSockets/internal/DLLExport.hpp index 70f77ee..97a17ad 100644 --- a/include/CppSockets/internal/DLLExport.hpp +++ b/include/CppSockets/internal/DLLExport.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Jul 1 12:30:17 2026 Francois Michaut -** Last update Wed Jul 1 18:54:43 2026 Francois Michaut +** Last update Sun Oct 25 23:15:42 2026 Francois Michaut ** ** DLLExport.hpp : Macros for DLL symbol exporting */ diff --git a/include/CppSockets/internal/SocketInit.hpp b/include/CppSockets/internal/SocketInit.hpp index 82eae29..284ffae 100644 --- a/include/CppSockets/internal/SocketInit.hpp +++ b/include/CppSockets/internal/SocketInit.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:05 2026 Francois Michaut +** Last update Sun Oct 25 23:15:50 2026 Francois Michaut ** ** SocketInit.hpp : Socket class automatic initialization and teardown */ diff --git a/private/CppSockets/SocketInternals.hpp b/private/CppSockets/SocketInternals.hpp new file mode 100644 index 0000000..517e503 --- /dev/null +++ b/private/CppSockets/SocketInternals.hpp @@ -0,0 +1,44 @@ +/* +** Project LibCppSockets, 2022 +** +** Author Francois Michaut +** +** Started on Mon Oct 26 11:05:41 2026 Francois Michaut +** Last update Mon Oct 26 21:41:31 2026 Francois Michaut +** +** SocketInternals.hpp : Private constants shared by Socket implementations +*/ + +#pragma once + +#include "CppSockets/OSDetection.hpp" + +#include + +#ifdef OS_WINDOWS + #include +#else + #include +#endif + +namespace CppSockets { + static constexpr std::size_t BUFF_SIZE = 4096; + + // Normalized socket error codes. get_errno() returns WSAGetLastError() + // on Windows and errno for linux. Using unified constants simplifies portable logic. +#ifdef OS_WINDOWS + static constexpr int SOCK_EINTR = WSAEINTR; + static constexpr int SOCK_EAGAIN = WSAEWOULDBLOCK; // Winsock has no EAGAIN + static constexpr int SOCK_EWOULDBLOCK = WSAEWOULDBLOCK; + static constexpr int SOCK_EINPROGRESS = WSAEINPROGRESS; + static constexpr int SOCK_EALREADY = WSAEALREADY; + static constexpr int SOCK_EISCONN = WSAEISCONN; +#else + static constexpr int SOCK_EINTR = EINTR; + static constexpr int SOCK_EAGAIN = EAGAIN; + static constexpr int SOCK_EWOULDBLOCK = EWOULDBLOCK; + static constexpr int SOCK_EINPROGRESS = EINPROGRESS; + static constexpr int SOCK_EALREADY = EALREADY; + static constexpr int SOCK_EISCONN = EISCONN; +#endif +} diff --git a/source/Address.cpp b/source/Address.cpp index 0d49030..7c2c6fb 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 Sun Oct 25 12:47:04 2026 Francois Michaut +** Last update Mon Oct 26 12:40:08 2026 Francois Michaut ** ** Address.cpp : Implementation of generic Address classes & functions */ @@ -12,7 +12,8 @@ #include "CppSockets/Address.hpp" #include "CppSockets/IPv4.hpp" -#include +#include +#include namespace CppSockets { auto IEndpoint::make_string() const -> std::string { @@ -20,34 +21,30 @@ namespace CppSockets { return this->get_addr().to_string() + ":" + std::to_string(this->get_port()); } - // TODO: Find a better way to use string_view auto IEndpoint::from_string(std::string_view endpoint) -> std::shared_ptr { - return from_string(std::string(endpoint)); - } - - 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) { + if (colon_pos == 0 || colon_pos == std::string::npos) { throw std::runtime_error("Invalid Endpoint"); } + auto port_text = endpoint.substr(colon_pos + 1); - std::stringstream ss{std::move(endpoint)}; - std::string ip_part(colon_pos, '\0'); // 0-init str with enough size - std::uint16_t port = 0; + if (port_text.empty()) { + throw std::runtime_error("Invalid Endpoint"); + } - ss.read(ip_part.data(), colon_pos); + IPv4 ipv4(endpoint.substr(0, colon_pos)); - IPv4 ipv4(ip_part.c_str()); + std::uint16_t port = 0; + const char *port_start = port_text.data(); + const char *port_end = port_start + port_text.size(); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + const auto [end, ec] = std::from_chars(port_start, port_end, port); - ss.ignore(1, ':'); - if (ss) { - ss >> port; + if (ec != std::errc() || end != port_end) { + throw std::runtime_error("Invalid port in endpoint"); } - // TODO: Error handling - return std::make_shared(ipv4, port); } } diff --git a/source/ErrorUtils.cpp b/source/ErrorUtils.cpp new file mode 100644 index 0000000..fe62a99 --- /dev/null +++ b/source/ErrorUtils.cpp @@ -0,0 +1,72 @@ +/* +** Project LibCppSockets, 2026 +** +** Author Francois Michaut +** +** Started on Mon Oct 26 22:28:56 2026 Francois Michaut +** Last update Mon Oct 26 23:02:45 2026 Francois Michaut +** +** ErrorUtils.cpp : Portable system error utilities implementation +*/ + +#include "CppSockets/ErrorUtils.hpp" +#include "CppSockets/OSDetection.hpp" + +#ifdef OS_WINDOWS + #include +#else + #include +#endif + +#include + +namespace { + class socket_error_category final : public std::error_category { + public: + auto name() const noexcept -> const char * override { +#ifdef OS_WINDOWS + return "Winsock"; +#else + return "errno"; +#endif + } + + auto message(int err) const -> std::string override { +#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 + return std::system_category().message(err); +#endif + } + }; + + auto socket_category() -> const std::error_category & { + static const socket_error_category instance{}; + return instance; + } +} + +namespace CppSockets { + auto strerror(int err) -> std::string { + return std::error_code(err, socket_category()).message(); + } + + auto get_errno() -> int { +#ifdef OS_WINDOWS + return WSAGetLastError(); +#else + return errno; +#endif + } + + auto strerror() -> std::string { + return strerror(get_errno()); + } +} diff --git a/source/IPv4.cpp b/source/IPv4.cpp index 32900fc..90a778c 100644 --- a/source/IPv4.cpp +++ b/source/IPv4.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sun Feb 13 18:52:28 2022 Francois Michaut -** Last update Wed Jul 1 14:19:56 2026 Francois Michaut +** Last update Mon Oct 26 14:27:02 2026 Francois Michaut ** ** IPv4.cpp : Implementation of IPv4 class */ @@ -21,22 +21,32 @@ #include #endif +constexpr std::uint8_t MAX_ADDR_SIZE = 16; + namespace CppSockets { IPv4::IPv4(std::uint32_t addr) : - addr(htonl(addr)) // TODO: We are assuming input is always host - how do we allow network byte order as input ? + addr(addr) { - std::array buff = {0}; + std::array buff = {0}; inet_ntop(AF_INET, &this->addr, buff.data(), buff.size()); str = buff.data(); } - IPv4::IPv4(const char *addr) : + auto IPv4::from_host_order(std::uint32_t addr) -> IPv4 { + return IPv4(htonl(addr)); + } + + auto IPv4::from_network_order(std::uint32_t addr) -> IPv4 { + return IPv4(addr); + } + + IPv4::IPv4(std::string_view addr) : str(addr) { struct in_addr address = {}; - if (inet_pton(AF_INET, addr, &address) != 1) + if (inet_pton(AF_INET, str.c_str(), &address) != 1) throw std::runtime_error("Invalid IPv4 address"); this->addr = address.s_addr; } diff --git a/source/Socket.cpp b/source/Socket.cpp index e9cf847..5031ff5 100644 --- a/source/Socket.cpp +++ b/source/Socket.cpp @@ -4,24 +4,28 @@ ** Author Francois Michaut ** ** Started on Sat Jan 15 01:27:40 2022 Francois Michaut -** Last update Sun Oct 25 13:27:58 2026 Francois Michaut +** Last update Mon Oct 26 18:30:50 2026 Francois Michaut ** ** Socket.cpp : Protable C++ socket class implementation */ #include "CppSockets/IPv4.hpp" +#include "CppSockets/ErrorUtils.hpp" #include "CppSockets/OSDetection.hpp" #include "CppSockets/Socket.hpp" +#include "CppSockets/SocketInternals.hpp" #ifdef OS_WINDOWS #include #include + // Match linux defines #define SHUT_RDWR SD_BOTH #else #include #include #include + #include #include // To match windows's constants @@ -29,13 +33,11 @@ static constexpr int SOCKET_ERROR = -1; #endif -static constexpr int BUFF_SIZE = 4096; - +#include #include #include #include #include -#include #include // TODO add exceptions on error retunrs @@ -44,11 +46,11 @@ namespace CppSockets { Socket::Socket(RawSocketType sockfd, bool connected) : m_sockfd(sockfd), m_is_connected(connected) { - Socket::getsockopt(sockfd, SOL_SOCKET, SO_TYPE, reinterpret_cast(&m_type), sizeof(int)); + Socket::getsockopt(sockfd, SOL_SOCKET, SO_TYPE, reinterpret_cast(&m_type), static_cast(sizeof(int))); #ifdef SO_DOMAIN // Fast path for Linux - Socket::getsockopt(sockfd, SOL_SOCKET, SO_DOMAIN, &m_domain, sizeof(int)); + Socket::getsockopt(sockfd, SOL_SOCKET, SO_DOMAIN, &m_domain, static_cast(sizeof(int))); #else struct sockaddr_storage addr{}; socklen_t len = sizeof(addr); @@ -60,7 +62,7 @@ namespace CppSockets { #ifdef SO_PROTOCOL // Fast path for Linux - Socket::getsockopt(sockfd, SOL_SOCKET, SO_PROTOCOL, &m_protocol, sizeof(int)); + Socket::getsockopt(sockfd, SOL_SOCKET, SO_PROTOCOL, &m_protocol, static_cast(sizeof(int))); #elif defined (OS_WINDOWS) // Windows fallback using Protocol Info WSAPROTOCOL_INFO info; @@ -116,16 +118,16 @@ namespace CppSockets { } 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'; + if (m_sockfd != INVALID_SOCKET) { + 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: " << CppSockets::strerror() << '\n'; + } + m_is_connected = false; } - m_is_connected = false; - } - if (m_sockfd != INVALID_SOCKET) { #ifdef OS_WINDOWS int res = closesocket(m_sockfd); #else @@ -143,19 +145,24 @@ namespace CppSockets { std::array buff; std::size_t buff_size = buff.size(); std::string res; - std::size_t nb = 1; + std::size_t nb = 0; + bool read_all_avail = len == static_cast(-1); - if (len != -1) res.reserve(len); + if (!read_all_avail) + res.reserve(len); - while (nb != 0 && len != 0) { - nb = this->read(buff.data(), (buff_size > len ? len : buff_size)); - if (nb > 0) { - res.append(buff.data(), nb); - if (len != -1) { - len -= nb; - } + do { + nb = this->read(buff.data(), read_all_avail ? buff_size : std::min(buff_size, len)); + + if (nb == 0) { + break; } - } + res.append(buff.data(), nb); + + if (!read_all_avail) { + len -= nb; + } + } while (read_all_avail ? this->has_more_data() : len != 0); return res; } @@ -165,16 +172,28 @@ namespace CppSockets { if (!m_is_connected) throw std::runtime_error("Not connected"); ret = ::read(m_sockfd, buff, size); - if (ret < 0) + if (ret < 0) { + int err = CppSockets::get_errno(); + + if (err == SOCK_EAGAIN || err == SOCK_EWOULDBLOCK) { + return 0; + } throw_errno_error("Failed to read from socket: "); - if (ret == 0 && size > 0) { + } else if (ret == 0 && size > 0) { m_is_connected = false; } return ret; } - auto Socket::write(std::string_view buff) -> std::size_t { - return this->write(buff.data(), buff.size()); + auto Socket::has_more_data() const -> bool { + try { + return static_cast(this->poll(PollEvent::READ)); + } catch (const std::runtime_error &) { + // If poll errors now, something is wrong on the socket. + // We return false to interupt the read loop, and return what we got, + // and the error will be raised by the next I/O call. + return false; + } } auto Socket::write(const char *buff, std::size_t len) -> std::size_t { // NOLINT(readability-make-member-function-const) @@ -183,8 +202,14 @@ namespace CppSockets { if (!m_is_connected) throw std::runtime_error("Not connected"); ret = ::write(m_sockfd, buff, len); - if (ret < 0) + if (ret < 0) { + int err = CppSockets::get_errno(); + + if (err == SOCK_EAGAIN || err == SOCK_EWOULDBLOCK) { + return 0; + } throw_errno_error("Failed to write to socket: "); + } return ret; } @@ -207,12 +232,14 @@ namespace CppSockets { } auto Socket::bind(const std::string &addr, uint16_t port) -> int { - return this->bind(inet_addr(addr.c_str()), port); + return this->bind(Endpoint(IPv4(addr.c_str()), port)); } auto Socket::bind(const IEndpoint &endpoint) -> int { // TODO: this only works for IPv4. Need to switch getFamily() to handle // IPv6 / AF_UNIX ... + if (endpoint.get_addr().get_family() != AF_INET) + throw std::runtime_error("Only AF_INET endpoints are supported"); return this->bind(endpoint.get_addr().get_address(), endpoint.get_port()); } @@ -222,21 +249,42 @@ namespace CppSockets { auto Socket::connect(const IEndpoint &endpoint) -> int { struct sockaddr_in addr = {0}; + auto &address = endpoint.get_addr(); int ret = 0; - addr.sin_addr.s_addr = endpoint.get_addr().get_address(); + if (m_domain != AF_INET) + throw std::runtime_error("Socket is not an AF_INET socket"); + if (address.get_family() != AF_INET) + throw std::runtime_error("Only AF_INET endpoints are supported"); + + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = address.get_address(); addr.sin_port = htons(endpoint.get_port()); - addr.sin_family = endpoint.get_addr().get_family(); - if (m_is_connected) { - this->close(); + if (m_is_connected && m_type == SOCK_STREAM) { + this->close(); // TCP cannot re-use socket (EISCONN), need to close first } if (m_sockfd == INVALID_SOCKET) { this->create_socket(); } ret = ::connect(m_sockfd, reinterpret_cast(&addr), sizeof(addr)); if (ret < 0) { - this->close(); - throw_errno_error(std::string("Failed to connect socket to ") + endpoint.to_string() + " : "); + int connect_errno = CppSockets::get_errno(); + + m_is_connected = false; + // handle "in-progress" connect errors for non-blocking sockets + if (connect_errno == SOCK_EINPROGRESS || connect_errno == SOCK_EALREADY) { + return 0; + } else if (connect_errno == SOCK_EISCONN) { + m_is_connected = true; + return 0; + } + try { + this->close(); + } catch (const std::runtime_error &e) { + std::cerr << "Exception while closing failed connect socket: " << e.what() << '\n'; + } + + throw_errno_error(std::string("Failed to connect socket to ") + endpoint.to_string() + " : ", connect_errno); } else { m_is_connected = true; } @@ -251,20 +299,22 @@ namespace CppSockets { return ret; } - auto Socket::accept(void *addr_out) -> std::unique_ptr { - return std::unique_ptr(acceptImpl(addr_out)); + auto Socket::accept() -> std::unique_ptr { + return std::unique_ptr(acceptImpl(nullptr)); } - auto Socket::acceptImpl(void *addr_out) -> Socket * { // NOLINT(readability-make-member-function-const) - int fd = ::accept(m_sockfd, nullptr, nullptr); + auto Socket::accept(sockaddr_storage &addr) -> std::unique_ptr { + return std::unique_ptr(acceptImpl(&addr)); + } + + auto Socket::acceptImpl(sockaddr_storage *addr_out) -> Socket * { // NOLINT(readability-make-member-function-const) + socklen_t addr_len = static_cast(sizeof(sockaddr_storage)); + int fd = ::accept(m_sockfd, reinterpret_cast(addr_out), &addr_len); - if (addr_out != nullptr) { - // TODO figure it out - } if (fd == INVALID_SOCKET) { - int err = this->get_errno(); + int err = CppSockets::get_errno(); - if (err == EAGAIN || err == EWOULDBLOCK || err == EINTR) { + if (err == SOCK_EAGAIN || err == SOCK_EWOULDBLOCK || err == SOCK_EINTR) { return nullptr; } throw_errno_error("Failed to accept: ", err); @@ -296,6 +346,43 @@ namespace CppSockets { #endif } + auto Socket::poll(PollEvent events, int timeout_ms) const -> PollEvent { + PollfdType pfd{}; + int ret; + + pfd.fd = m_sockfd; + if (events.has(PollEvent::READ)) pfd.events |= POLLIN; + if (events.has(PollEvent::WRITE)) pfd.events |= POLLOUT; + +#ifdef OS_WINDOWS + ret = WSAPoll(&pfd, 1, timeout_ms); +#else + ret = ::poll(&pfd, 1, timeout_ms); +#endif + if (ret < 0) { + throw_errno_error("Failed to poll socket: "); + } else if (ret == 0) { + return PollEvent::NONE; + } + + if (pfd.revents & POLLNVAL) { + throw std::runtime_error("Socket fd is not valid"); + } + if (pfd.revents & POLLERR) { + int sock_err = 0; + + Socket::getsockopt(m_sockfd, SOL_SOCKET, SO_ERROR, reinterpret_cast(&sock_err), static_cast(sizeof(int))); + throw_errno_error("Socket error: ", sock_err); + } + + PollEvent result = PollEvent::NONE; + + if (pfd.revents & (POLLIN | POLLHUP)) result |= PollEvent::READ; + if (pfd.revents & POLLOUT) result |= PollEvent::WRITE; + + return result & events; + } + auto Socket::getsockopt(int fd, int level, int optname, SockOptType *optval, socklen_t *optlen) -> int { int ret = ::getsockopt(fd, level, optname, optval, optlen); @@ -308,7 +395,9 @@ namespace CppSockets { struct sockaddr_in addr = {}; int ret = 0; - addr.sin_family = m_domain; + if (m_domain != AF_INET) + throw std::runtime_error("Socket is not an AF_INET socket"); + addr.sin_family = AF_INET; addr.sin_addr.s_addr = source_addr; addr.sin_port = htons(port); ret = ::bind(m_sockfd, reinterpret_cast(&addr), sizeof(addr)); @@ -318,41 +407,12 @@ namespace CppSockets { 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()); + Socket::throw_errno_error(std::move(message), CppSockets::get_errno()); } void Socket::throw_errno_error(std::string message, int err) { - throw std::runtime_error(std::move(message) + Socket::strerror(err)); + throw std::runtime_error(std::move(message) + CppSockets::strerror(err)); } void Socket::create_socket() { diff --git a/source/SocketInit.cpp b/source/SocketInit.cpp index 498565f..806dfa8 100644 --- a/source/SocketInit.cpp +++ b/source/SocketInit.cpp @@ -4,11 +4,12 @@ ** Author Francois Michaut ** ** Started on Thu Sep 15 14:24:25 2022 Francois Michaut -** Last update Thu Oct 22 22:06:42 2026 Francois Michaut +** Last update Mon Oct 26 22:48:19 2026 Francois Michaut ** ** init.cpp : Startup/Cleanup functions implementation */ +#include "CppSockets/ErrorUtils.hpp" #include "CppSockets/OSDetection.hpp" #include "CppSockets/internal/SocketInit.hpp" @@ -19,7 +20,6 @@ #include #ifdef OS_WINDOWS - #include #include #include @@ -34,9 +34,9 @@ namespace CppSockets { WSADATA wsa_data; // TODO check value in wsa_data - if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) { - // TODO: use WSAGetLastError - init_error = std::string("WASStartup Failed : ") + std::strerror(errno); + const int wsa_error = WSAStartup(MAKEWORD(2, 2), &wsa_data); + if (wsa_error != 0) { + init_error = std::string("WSAStartup Failed : ") + CppSockets::strerror(wsa_error); std::cerr << init_error << '\n'; return false; } @@ -53,8 +53,7 @@ namespace CppSockets { static void deinit() { #ifdef OS_WINDOWS if (WSACleanup() == SOCKET_ERROR) { - // TODO use FormatMessage to get the error string - std::cerr << std::string("WSACleanup Failed : ") << std::to_string(WSAGetLastError()) << '\n'; + std::cerr << std::string("WSACleanup Failed : ") << CppSockets::strerror() << '\n'; } #else // TODO check return values / raise errors diff --git a/source/Tls/Callbacks.cpp b/source/Tls/Callbacks.cpp index d8d9c44..56ffba0 100644 --- a/source/Tls/Callbacks.cpp +++ b/source/Tls/Callbacks.cpp @@ -4,7 +4,7 @@ ** 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 +** Last update Mon Oct 26 11:46:20 2026 Francois Michaut ** ** Callbacks.cpp : Centralization of the boilerplate for verify callbacks */ @@ -47,28 +47,30 @@ 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_ex_data(m_ptr.get(), TLS_CONTEXT_CALLBACK_STRUCT_IDX, nullptr); SSL_CTX_set_verify(m_ptr.get(), mode, nullptr); } + delete static_cast(previous); } 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_ex_data(m_ssl.get(), TLS_SOCKET_CALLBACK_STRUCT_IDX, nullptr); SSL_set_verify(m_ssl.get(), mode, nullptr); } + delete static_cast(previous); } } diff --git a/source/Tls/Certificate.cpp b/source/Tls/Certificate.cpp index f0cbde7..e15f8be 100644 --- a/source/Tls/Certificate.cpp +++ b/source/Tls/Certificate.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Sat Aug 2 22:41:35 2025 Francois Michaut -** Last update Thu Jul 2 03:01:04 2026 Francois Michaut +** Last update Sun Oct 25 23:12:25 2026 Francois Michaut ** ** Certificate.cpp : Implementation of classes to create and manage Certificates */ @@ -18,16 +18,15 @@ #include #include #include +#include namespace { template inline auto numeric_cast(const Src value) -> Dst { - const Dst result = static_cast(value); - - if (result != value) { + if (!std::in_range(value)) { throw std::overflow_error("Overflow/Underflow error"); } - return result; + return static_cast(value); } } @@ -52,7 +51,7 @@ namespace CppSockets { } x509Name::x509Name(const X509_NAME *ptr) : - m_ptr(const_cast(ptr)), m_own(false) + m_ptr(X509_NAME_dup(const_cast(ptr))) { REQUIRED_PTR(m_ptr, "X509_NAME") } @@ -145,7 +144,7 @@ namespace CppSockets { } x509NameEntry::x509NameEntry(const X509_NAME_ENTRY *ptr) : - m_ptr(const_cast(ptr)), m_own(false) + m_ptr(X509_NAME_ENTRY_dup(const_cast(ptr))) { REQUIRED_PTR(m_ptr, "X509_NAME_ENTRY") } @@ -216,7 +215,7 @@ namespace CppSockets { } x509Extension::x509Extension(const X509_EXTENSION *ptr) : - m_ptr(const_cast(ptr)), m_own(false) + m_ptr(X509_EXTENSION_dup(const_cast(ptr))) { REQUIRED_PTR(m_ptr, "X509_EXTENSION") } @@ -285,7 +284,7 @@ namespace CppSockets { } x509Certificate::x509Certificate(const X509 *ptr) : - m_ptr(const_cast(ptr)), m_own(false) + m_ptr(X509_dup(const_cast(ptr))) { REQUIRED_PTR(m_ptr, "X509") } @@ -357,7 +356,7 @@ namespace CppSockets { void x509Certificate::set_not_before(int offset_day, std::int64_t offset_sec, time_t *in_tm) { ASN1_TIME *not_before = X509_getm_notBefore(m_ptr.get()); - if (!X509_time_adj_ex(not_before, offset_day, offset_sec, in_tm)) { + if (!X509_time_adj_ex(not_before, offset_day, numeric_cast(offset_sec), in_tm)) { throw std::runtime_error("Failed to adjust not_before time"); } } @@ -365,13 +364,13 @@ namespace CppSockets { void x509Certificate::set_not_after(int offset_day, std::int64_t offset_sec, time_t *in_tm) { ASN1_TIME *not_after = X509_getm_notAfter(m_ptr.get()); - if (!X509_time_adj_ex(not_after, offset_day, offset_sec, in_tm)) { + if (!X509_time_adj_ex(not_after, offset_day, numeric_cast(offset_sec), in_tm)) { throw std::runtime_error("Failed to adjust not_after time"); } } void x509Certificate::set_version(std::int64_t version) { - if (!X509_set_version(m_ptr.get(), version)) { + if (!X509_set_version(m_ptr.get(), numeric_cast(version))) { throw std::runtime_error("Failed to set version"); } } diff --git a/source/Tls/Context.cpp b/source/Tls/Context.cpp index 45fac4e..cf5b6fc 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 Sun Oct 25 00:18:04 2026 Francois Michaut +** Last update Mon Oct 26 15:28:35 2026 Francois Michaut ** ** Context.cpp : Implementation of the Context for TLS sockets */ @@ -28,6 +28,13 @@ namespace CppSockets { m_ptr(SSL_CTX_new(TLS_method())) { TLS_CONTEXT_CONSTRUCTOR_BODY; + + // TlsContext safe by default. Enable verify and uses system trust store. + // Also set decent security defaults, can be overriden by the library user. + SSL_CTX_set_verify(m_ptr.get(), SSL_VERIFY_PEER, nullptr); + SSL_CTX_set_default_verify_paths(m_ptr.get()); + SSL_CTX_set_dh_auto(m_ptr.get(), 1); + SSL_CTX_set_cipher_list(m_ptr.get(), "HIGH:!aNULL:!eNULL:!MD5:!RC4:!3DES"); } TlsContext::TlsContext(SSL_CTX_ptr ptr) : @@ -69,6 +76,18 @@ namespace CppSockets { } } + void TlsContext::set_cipher_list(const char *cipher_list) { + if (!SSL_CTX_set_cipher_list(m_ptr.get(), cipher_list)) { + throw std::runtime_error("Failed to set cipher list"); + } + } + + void TlsContext::set_ciphersuites(const char *ciphersuites) { + if (!SSL_CTX_set_ciphersuites(m_ptr.get(), ciphersuites)) { + throw std::runtime_error("Failed to set TLS 1.3 ciphersuites"); + } + } + 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 07a7523..1d2dd76 100644 --- a/source/Tls/Socket.cpp +++ b/source/Tls/Socket.cpp @@ -4,50 +4,35 @@ ** Author Francois Michaut ** ** Started on Wed Sep 14 21:04:42 2022 Francois Michaut -** Last update Sun Oct 25 12:30:49 2026 Francois Michaut +** Last update Mon Oct 26 21:38:10 2026 Francois Michaut ** ** SecureSocket.cpp : TLS socket wrapper implementation */ #include "CppSockets/OSDetection.hpp" +#include "CppSockets/SocketInternals.hpp" #include "CppSockets/Tls/Context.hpp" #include "CppSockets/Tls/Socket.hpp" #include "CppSockets/Tls/Utils.hpp" #include #include +#include -#include +#include #include -#include #include -static constexpr int BUFF_SIZE = 4096; - namespace { - auto extract_errors_from_queue() -> std::string { - auto error = ERR_get_error(); - std::stringstream ss; - - while (error != 0) { - ss << ERR_error_string(error, nullptr); - error = ERR_get_error(); - if (error != 0) { - ss << '\n'; - } - } - return ss.str(); - } - void init_ssl_socket(SSL *ssl, CppSockets::TlsSocket *socket) { if (!ssl) { - throw std::runtime_error("Failed to initialize TLS socket: " + extract_errors_from_queue()); + throw std::runtime_error("Failed to initialize TLS socket: " + CppSockets::openssl_extract_errors_from_stack()); } if (!SSL_set_fd(ssl, socket->get_fd())) { - throw std::runtime_error(std::string("Failed to initialize TLS socket: ") + socket->tls_strerror(0)); + throw std::runtime_error(std::string("Failed to initialize TLS socket: ") + CppSockets::ssl_strerror(socket->tls_errno(0))); } 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)); + throw std::runtime_error(std::string("Failed to select TLS version: ") + CppSockets::ssl_strerror(socket->tls_errno(0))); } } } @@ -66,7 +51,8 @@ namespace CppSockets { 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) + m_should_shutdown(other.m_should_shutdown), + m_pending_accept(std::move(other.m_pending_accept)) {} TlsSocket::TlsSocket(Socket &&other, TlsContext ctx) : @@ -78,7 +64,7 @@ namespace CppSockets { TlsSocket::TlsSocket(Socket &&other, SSL_ptr ssl) : CppSockets::Socket(std::move(other)), // TODO: if socket is not connected, at that moment, does it break ? - m_ctx({ssl ? SSL_get_SSL_CTX(ssl.get()) : SSL_CTX_new(TLS_method()), !ssl}), + m_ctx(ssl ? static_cast(TlsContext(SSL_get_SSL_CTX(ssl.get()), false)) : SSL_CTX_new(TLS_method())), m_ssl(ssl ? std::move(ssl) : SSL_ptr(SSL_new(m_ctx.get()))), m_peer_cert(nullptr) { @@ -97,6 +83,7 @@ namespace CppSockets { m_ssl = std::move(other.m_ssl); m_ctx = std::move(other.m_ctx); m_peer_cert = std::move(other.m_peer_cert); + m_pending_accept = std::move(other.m_pending_accept); std::swap(m_should_shutdown, other.m_should_shutdown); @@ -104,7 +91,7 @@ namespace CppSockets { return *this; } - bool TlsSocket::close(bool force) { + auto TlsSocket::close(bool force) -> bool { if (m_ssl && m_should_shutdown) { int ret = SSL_shutdown(m_ssl.get()); @@ -119,6 +106,19 @@ namespace CppSockets { } else if (this->connected()) { Socket::close(); } + + m_should_shutdown = false; + m_peer_cert.reset(); + m_pending_accept.reset(); + + if (m_ssl) { + // Properly reset the SSL object so it can be reused by a subsequent + // connect call to a different host + this->set_hostname(nullptr); + X509_VERIFY_PARAM_set1_ip(SSL_get0_param(m_ssl.get()), nullptr, 0); + if (!SSL_clear(m_ssl.get())) + throw std::runtime_error("Failed to reset SSL: " + openssl_extract_errors_from_stack()); + } return true; } @@ -126,6 +126,16 @@ namespace CppSockets { SSL_set_verify_depth(m_ssl.get(), depth); } + void TlsSocket::set_hostname(const char *hostname) { +#if OPENSSL_VERSION_NUMBER >= 0x40000000L + if (!SSL_set1_dnsname(m_ssl.get(), hostname)) { +#else + if (!SSL_set1_host(m_ssl.get(), hostname)) { +#endif + throw_tls_error("Failed to set hostname: "); + } + } + void TlsSocket::set_certificate(const std::string &cert_path, const std::string &pkey_path) { BIO_ptr cert(BIO_new_file(cert_path.c_str(), "r")); BIO_ptr pkey(BIO_new_file(pkey_path.c_str(), "r")); @@ -142,49 +152,34 @@ namespace CppSockets { } } - // TODO add SSL_get_shutdown checks in read operations - auto TlsSocket::read(std::size_t len) -> std::string { - 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) { - 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 (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) { - len -= nb; - } - } - return res; - } - auto TlsSocket::read(char *buff, std::size_t size) -> std::size_t { std::size_t nb = 0; int ret = SSL_read_ex(m_ssl.get(), buff, size, &nb); - check_for_error("Failed to read from socket: ", ret); - return nb; + return handle_ssl_io_result("Failed to read from socket: ", ret, nb); + } + + auto TlsSocket::has_more_data() const -> bool { + if (SSL_pending(m_ssl.get()) > 0) { + return true; + } + try { + return static_cast(this->poll(PollEvent::READ)); + } catch (const std::runtime_error &) { + // A poll error after at least one successful read must not discard + // the data already read; stop draining and return it. + return false; + } } auto TlsSocket::write(const char *buff, std::size_t len) -> std::size_t { std::size_t nb = 0; int ret = SSL_write_ex(m_ssl.get(), static_cast(buff), len, &nb); - check_for_error("Failed to write to socket: ", ret); - return nb; + return handle_ssl_io_result("Failed to write to socket: ", ret, nb); } - void TlsSocket::check_for_error(std::string error_msg, int ret) { + auto TlsSocket::handle_ssl_io_result(std::string error_msg, int ssl_ret, std::size_t nb) -> std::size_t { int shutdown = SSL_get_shutdown(m_ssl.get()); if (shutdown == SSL_RECEIVED_SHUTDOWN) { @@ -192,85 +187,106 @@ namespace CppSockets { m_should_shutdown = false; } - if (ret <= 0) { - throw_tls_error(std::move(error_msg), ret); + if (ssl_ret > 0) { + return nb; + } + + int err = tls_errno(ssl_ret); + + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return 0; // Non-blocking mode: caller must retry the operation later. + } + if (err == SSL_ERROR_ZERO_RETURN) { + // Peer sent close_notify and closed the connection cleanly. + SSL_shutdown(m_ssl.get()); // TODO: log failure + m_should_shutdown = false; + return 0; } + throw_tls_error(std::move(error_msg), ssl_ret); + return 0; } auto TlsSocket::connect(const IEndpoint &endpoint) -> int { - int ret = Socket::connect(endpoint); + // Drop the old session when reconnecting to a new endpoint + if (SSL_is_init_finished(m_ssl.get())) { + this->close(true); + } + + if (!this->connected()) { + Socket::connect(endpoint); + if (!this->connected()) { + return 0; // Non-blocking tcp still connecting + } + } + + // If we reconnected the TCP socket, we need to bind the new fd to SSL + if (SSL_get_fd(m_ssl.get()) != this->get_fd()) { + if (!SSL_set_fd(m_ssl.get(), this->get_fd())) { + throw std::runtime_error(std::string("Failed to bind SSL to socket: ") + openssl_extract_errors_from_stack()); + } + } + int ssl_ret = SSL_connect(m_ssl.get()); - if (ssl_ret != 1) { - throw_tls_error("Failed to connect: ", ssl_ret); + if (ssl_ret == 1) { + m_should_shutdown = true; + m_peer_cert.reset(SSL_get_peer_certificate(m_ssl.get())); + return 1; + } + + int err = tls_errno(ssl_ret); + + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return 0; // Non-blocking handshake: need to poll() then call connect() again } - m_should_shutdown = true; - m_peer_cert.reset(SSL_get_peer_certificate(m_ssl.get())); - return ret; + throw_tls_error("Failed to connect: ", ssl_ret); + return 0; + } + + auto TlsSocket::accept(TlsContext ctx) -> std::unique_ptr { + return std::unique_ptr(this->acceptImpl(nullptr, std::move(ctx))); } - auto TlsSocket::accept(void *addr_out, TlsContext ctx) -> std::unique_ptr { - return std::unique_ptr(this->acceptImpl(addr_out, ctx)); + auto TlsSocket::accept(sockaddr_storage &addr_out, TlsContext ctx) -> std::unique_ptr { + return std::unique_ptr(this->acceptImpl(&addr_out, std::move(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; + auto TlsSocket::acceptImpl(sockaddr_storage *addr_out, TlsContext ctx) -> TlsSocket * { + if (!m_pending_accept) { + std::unique_ptr res{Socket::acceptImpl(addr_out)}; - if (!res) { - return nullptr; + if (!res) { + return nullptr; + } + m_pending_accept = std::make_unique(std::move(*res), std::move(ctx)); } - tls = std::make_unique(std::move(*res), std::move(ctx)); - ssl_ret = SSL_accept(tls->get_ssl().get()); - if (ssl_ret <= 0) { - throw_tls_error("Failed to accept TLS connection: ", ssl_ret); + int ssl_ret = SSL_accept(m_pending_accept->get_ssl().get()); + + if (ssl_ret == 1) { + m_pending_accept->m_should_shutdown = true; + m_pending_accept->m_peer_cert.reset(SSL_get_peer_certificate(m_pending_accept->get_ssl().get())); + return m_pending_accept.release(); } - tls->m_should_shutdown = true; - tls->m_peer_cert.reset(SSL_get_peer_certificate(tls->get_ssl().get())); - return tls.release(); + + int err = m_pending_accept->tls_errno(ssl_ret); + + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return nullptr; // Non-blocking handshake: need to poll() then call accept() again + } + m_pending_accept->throw_tls_error("Failed to accept TLS connection: ", ssl_ret); + return nullptr; } auto TlsSocket::get_ssl_ctx() const -> TlsContext { return {SSL_get_SSL_CTX(m_ssl.get()), false}; } - 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) { - case SSL_ERROR_NONE: - return "No error"; - case SSL_ERROR_ZERO_RETURN: - return "Peer closed TLS connection"; - case SSL_ERROR_WANT_READ: - return "Retry the operation later: WANT_READ"; - case SSL_ERROR_WANT_WRITE: - return "Retry the operation later: WANT_WRITE"; - case SSL_ERROR_WANT_CONNECT: - return "Retry the operation later: WANT_CONNECT"; - case SSL_ERROR_WANT_ACCEPT: - return "Retry the operation later: WANT_ACCEPT"; - case SSL_ERROR_WANT_X509_LOOKUP: - return "Retry the operation later: WANT_X509_LOOKUP"; - case SSL_ERROR_WANT_ASYNC: - return "Retry the operation later: WANT_ASYNC"; - case SSL_ERROR_WANT_ASYNC_JOB: - return "Retry the operation later: WANT_ASYNC_JOB"; - case SSL_ERROR_WANT_CLIENT_HELLO_CB: - return "Retry the operation later: WANT_CLIENT_HELLO_CB"; - case SSL_ERROR_SYSCALL: - m_should_shutdown = false; - return std::string("Fatal system error: ") + Socket::strerror() + '\n' + extract_errors_from_queue(); - case SSL_ERROR_SSL: - 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(); - } + auto TlsSocket::tls_errno(int ret) -> int { + if (!m_ssl) + throw std::runtime_error("SSL object is null"); + + return SSL_get_error(m_ssl.get(), ret); } void TlsSocket::throw_tls_error(std::string message) { @@ -278,6 +294,11 @@ namespace CppSockets { } void TlsSocket::throw_tls_error(std::string message, int ret) { - throw std::runtime_error(std::move(message) + tls_strerror(ret)); + int err = tls_errno(ret); + + if (err == SSL_ERROR_SYSCALL || err == SSL_ERROR_SSL) + m_should_shutdown = false; + + throw std::runtime_error(std::move(message) + ssl_strerror(err)); } } diff --git a/source/Tls/Utils.cpp b/source/Tls/Utils.cpp index fc918d0..e0dcc14 100644 --- a/source/Tls/Utils.cpp +++ b/source/Tls/Utils.cpp @@ -4,14 +4,16 @@ ** Author Francois Michaut ** ** Started on Sun Aug 3 20:36:03 2025 Francois Michaut -** Last update Wed Jul 1 14:20:18 2026 Francois Michaut +** Last update Mon Oct 26 00:17:24 2026 Francois Michaut ** ** SSL_Utils.cpp : SSL Utility implementations */ +#include "CppSockets/ErrorUtils.hpp" #include "CppSockets/Tls/Utils.hpp" #include +#include #include #include @@ -26,15 +28,29 @@ namespace CppSockets { // TODO: Use a custom exception instead of runtime error void throw_openssl_error() { - auto error = ERR_get_error(); - std::array buff = {0}; + std::string errors = openssl_extract_errors_from_stack(); - if (error == 0) + if (errors.empty()) { return; + } - ERR_error_string_n(error, buff.data(), buff.size()); + throw std::runtime_error(errors); + } - throw std::runtime_error(std::string(buff.data())); + auto openssl_extract_errors_from_stack() -> std::string { + auto error = ERR_get_error(); + std::array buff = {0}; + std::string errors; + + while (error != 0) { + ERR_error_string_n(error, buff.data(), buff.size()); + errors.append(buff.data()); + error = ERR_get_error(); + if (error != 0) { + errors += '\n'; + } + } + return errors; } auto check_or_throw_openssl_error(int ret) -> int { @@ -43,4 +59,35 @@ namespace CppSockets { } return ret; } + + auto ssl_strerror(int err) -> std::string { + switch (err) { + case SSL_ERROR_NONE: + return "No error"; + case SSL_ERROR_ZERO_RETURN: + return "Peer closed TLS connection"; + case SSL_ERROR_WANT_READ: + return "Retry the operation later: WANT_READ"; + case SSL_ERROR_WANT_WRITE: + return "Retry the operation later: WANT_WRITE"; + case SSL_ERROR_WANT_CONNECT: + return "Retry the operation later: WANT_CONNECT"; + case SSL_ERROR_WANT_ACCEPT: + return "Retry the operation later: WANT_ACCEPT"; + case SSL_ERROR_WANT_X509_LOOKUP: + return "Retry the operation later: WANT_X509_LOOKUP"; + case SSL_ERROR_WANT_ASYNC: + return "Retry the operation later: WANT_ASYNC"; + case SSL_ERROR_WANT_ASYNC_JOB: + return "Retry the operation later: WANT_ASYNC_JOB"; + case SSL_ERROR_WANT_CLIENT_HELLO_CB: + return "Retry the operation later: WANT_CLIENT_HELLO_CB"; + case SSL_ERROR_SYSCALL: + return std::string("Fatal system error: ") + CppSockets::strerror() + '\n' + openssl_extract_errors_from_stack(); + case SSL_ERROR_SSL: + return "Fatal TLS error: " + openssl_extract_errors_from_stack(); + default: + return "Unknown error: " + std::to_string(err) + '\n' + openssl_extract_errors_from_stack(); + } + } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1afea78..82bc106 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,7 +4,7 @@ ## Author Francois Michaut ## ## Started on Mon Feb 14 19:35:41 2022 Francois Michaut -## Last update Wed Jul 1 19:27:57 2026 Francois Michaut +## Last update Sun Oct 25 23:13:36 2026 Francois Michaut ## ## CMakeLists.txt : CMake building and running tests for CppSockets ## diff --git a/tests/TestSockets.cpp b/tests/TestSockets.cpp index 61b1060..eb76dc7 100644 --- a/tests/TestSockets.cpp +++ b/tests/TestSockets.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Mon Feb 14 21:17:55 2022 Francois Michaut -** Last update Wed Jul 1 19:02:32 2026 Francois Michaut +** Last update Sun Oct 25 23:13:24 2026 Francois Michaut ** ** TestSockets.cpp : Socket tests */ @@ -38,7 +38,7 @@ auto TestSockets(int /* ac */, char ** const /* av */) -> int soc.bind("0.0.0.0", port); soc.listen(1); - auto client = soc.accept(nullptr); + auto client = soc.accept(); auto buff = client->read(); if (buff != test) { diff --git a/tests/Tls/TestContext.cpp b/tests/Tls/TestContext.cpp index 73cd530..4e5a6c9 100644 --- a/tests/Tls/TestContext.cpp +++ b/tests/Tls/TestContext.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Fri Aug 22 21:09:12 2025 Francois Michaut -** Last update Mon Aug 10 22:28:37 2026 Francois Michaut +** Last update Sun Oct 25 23:13:47 2026 Francois Michaut ** ** TestContext.cpp : TlsContext tests */ diff --git a/tests/Tls/TestOpensslWrappers.cpp b/tests/Tls/TestOpensslWrappers.cpp index c6357fe..504f234 100644 --- a/tests/Tls/TestOpensslWrappers.cpp +++ b/tests/Tls/TestOpensslWrappers.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Jul 1 14:14:07 2026 Francois Michaut -** Last update Thu Jul 2 03:00:44 2026 Francois Michaut +** Last update Sun Oct 25 23:13:51 2026 Francois Michaut ** ** TestOpensslWrappers.cpp : OpenSSL wrappers tests */