diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..f069654 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,38 @@ +Checks: ' +*, +-llvmlibc-*, +-google-readability-todo, +-fuchsia-default-arguments-calls, +-fuchsia-default-arguments-declarations, +-cppcoreguidelines-init-variables, +-cppcoreguidelines-pro-type-reinterpret-cast, +-android-*, +-altera-*, +-llvm-namespace-comment, +-readability-implicit-bool-conversion, +-google-explicit-constructor, +-fuchsia-overloaded-operator, + +-google-readability-braces-around-statements, +-google-readability-namespace-comments, +-hicpp-braces-around-statements, +-hicpp-explicit-conversions, +-fuchsia-trailing-return +' + +WarningsAsErrors: 'bugprone-exception-escape' +FormatStyle: 'none' # TODO: Replace with 'file' once we have a proper .clang-format file +InheritParentConfig: true +CheckOptions: + misc-include-cleaner.MissingIncludes: 'false' + misc-include-cleaner.IgnoreHeaders: 'CppSockets/OSDetection\.hpp' + + bugprone-argument-comment.StrictMode: 1 + + # Readability + readability-braces-around-statements.ShortStatementLines: 2 + + readability-identifier-naming.NamespaceCase: CamelCase + + readability-identifier-length.IgnoredVariableNames: "^(fd|nb|ss)$" + readability-identifier-length.IgnoredParameterNames: "^([n]|fd)$" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..66dff6e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{c++,cc,cpp,cxx,h,h++,hh,hpp,hxx,inl,ipp,tlh,tli}] +cpp_indent_case_contents_when_block = true +cpp_new_line_before_open_brace_namespace = same_line +indent_size = 4 +indent_style = space diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 0a80f8b..4ba4323 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -4,6 +4,7 @@ on: push: branches: [ "master" ] pull_request: + branches: [ "master" ] types: [ "opened", "reopened", "synchronize", "ready_for_review" ] jobs: @@ -11,20 +12,21 @@ jobs: runs-on: ${{ matrix.os }} strategy: - fail-fast: false # ensure we don't stop after 1 failure to always have a complete picture of what is failing + # ensure we don't stop after 1 failure to always have a complete picture of what is failing + fail-fast: false # Set up a matrix to run the following configurations: # - ubuntu Debug/Release clang/gcc # - windows Debug/Release cl # - macos Debug/Release clang matrix: - os: [ubuntu-latest, macos-latest] # , windows-latest + os: [ubuntu-latest, macos-latest, windows-latest] build_type: [Release, Debug] c_compiler: [gcc, clang, cl] include: - # - os: windows-latest - # c_compiler: cl - # cpp_compiler: cl + - os: windows-latest + c_compiler: cl + cpp_compiler: cl - os: ubuntu-latest c_compiler: gcc cpp_compiler: g++ @@ -47,47 +49,82 @@ jobs: c_compiler: gcc steps: - - uses: actions/checkout@v3 - - - name: Set reusable strings - # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. - id: strings - shell: bash - run: | - echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" - - - name: Cache VCPKG (Windows) - if: runner.os == 'Windows' - uses: actions/cache@v3 - with: - path: ${{ env.VCPKG_ROOT }} - key: ${{ runner.os }}-${{ matrix.build_type }}-${{ hashFiles('vcpkg.json') }} - - - name: Install OpenSSL (Windows) - if: runner.os == 'Windows' - shell: powershell - run: | - echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append - echo "CMAKE_TOOLCHAIN_FILE=${env:VCPKG_INSTALLATION_ROOT}\scripts\buildsystems\vcpkg.cmake" | Out-File -FilePath $env:GITHUB_ENV -Append - vcpkg install - - - name: Configure CMake - # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. - # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: > - cmake -B ${{ steps.strings.outputs.build-output-dir }} - -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} - -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} - -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} - -DCPPSOCKETS_TESTS=TRUE - -S ${{ github.workspace }} - - - name: Build - # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). - run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} - - - name: Test - working-directory: ${{ steps.strings.outputs.build-output-dir }} - # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - run: ctest --build-config ${{ matrix.build_type }} --test-dir tests + - uses: actions/checkout@v3 + + - name: Set Env + shell: bash + run: | + echo "VCPKG_ROOT=${VCPKG_INSTALLATION_ROOT}" >> "$GITHUB_ENV" + echo "BUILD_OUTPUT_DIR=${{ github.workspace }}/build" >> "$GITHUB_ENV" + + - name: Fetch VCPKG Cache (Windows) + id: fetch-vcpkg-cache + if: runner.os == 'Windows' + uses: actions/cache/restore@v4 + with: + key: ${{ runner.os }}-${{ matrix.build_type }}-${{ hashFiles('vcpkg.json') }} + path: ${{ env.VCPKG_ROOT }} + + - name: Install OpenSSL (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' + uses: actions/cache/save@v4 + with: + key: ${{ steps.fetch-vcpkg-cache.outputs.cache-primary-key }} + path: ${{ env.VCPKG_ROOT }} + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. + # `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: > + cmake -B ${{ env.BUILD_OUTPUT_DIR }} + -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} + -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -DCPPSOCKETS_TESTS=TRUE + -S ${{ github.workspace }} + + - name: Build + # Build your program with the given configuration. Note that --config is needed + # because the default Windows generator is a multi-config generator (Visual Studio generator). + run: cmake --build ${{ env.BUILD_OUTPUT_DIR }} --config ${{ matrix.build_type }} + + - uses: actions/upload-artifact@v4 + if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'Release' && matrix.c_compiler == 'clang' + with: + name: compile_commands.json + path: ${{ env.BUILD_OUTPUT_DIR }}/compile_commands.json + + - name: Test + working-directory: ${{ env.BUILD_OUTPUT_DIR }} + # Execute tests defined by the CMake configuration. Note that --build-config is needed + # because the default Windows generator is a multi-config generator (Visual Studio generator). + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest --build-config ${{ matrix.build_type }} --test-dir tests --output-on-failure + + clang-tidy: + needs: 'build' + runs-on: ubuntu-latest + if: always() + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: compile_commands.json + + - name: clang-tidy review + uses: ZedThree/clang-tidy-review@v0.21.0 + + # If there are any comments, fail the check + - if: steps.review.outputs.total_comments > 0 + run: exit 1 diff --git a/.gitignore b/.gitignore index a5d6cc0..71b4e88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ build/ +libcppsockets.so compile_commands.json *.tmp *.gch +vgcore.* + +.vscode/ +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index ba37512..33895f2 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 Sat Dec 2 17:45:28 2023 Francois Michaut +## Last update Mon Aug 4 23:34:08 2025 Francois Michaut ## ## CMakeLists.txt : CMake to build the CppSockets library ## @@ -22,7 +22,9 @@ configure_file(include/CppSockets/Version.hpp.in include/CppSockets/Version.hpp) add_library(cppsockets source/Address.cpp + source/Certificate.cpp source/IPv4.cpp + source/SSL_Utils.cpp source/Socket.cpp source/SocketInit.cpp source/TlsSocket.cpp diff --git a/include/CppSockets/Address.hpp b/include/CppSockets/Address.hpp index a3ab14e..4561c7b 100644 --- a/include/CppSockets/Address.hpp +++ b/include/CppSockets/Address.hpp @@ -20,19 +20,19 @@ namespace CppSockets { class IAddress { public: - [[nodiscard]] virtual std::uint32_t getAddress() const = 0; - [[nodiscard]] virtual int getFamily() const = 0; - [[nodiscard]] virtual const std::string &toString() const = 0; + [[nodiscard]] virtual auto getAddress() const -> std::uint32_t = 0; + [[nodiscard]] virtual auto getFamily() const -> int = 0; + [[nodiscard]] virtual auto toString() const -> const std::string & = 0; }; class IEndpoint { public: - [[nodiscard]] virtual std::uint16_t getPort() const = 0; - [[nodiscard]] virtual const IAddress &getAddr() const = 0; - [[nodiscard]] virtual const std::string &toString() const = 0; + [[nodiscard]] virtual auto getPort() const -> std::uint16_t = 0; + [[nodiscard]] virtual auto getAddr() const -> const IAddress & = 0; + [[nodiscard]] virtual auto toString() const -> const std::string & = 0; protected: - [[nodiscard]] std::string makeString() const; + [[nodiscard]] auto makeString() const -> std::string; }; template @@ -46,15 +46,15 @@ namespace CppSockets { {}; virtual ~Endpoint() = default; - [[nodiscard]] std::uint16_t getPort() const override { + [[nodiscard]] auto getPort() const -> std::uint16_t override { return port; } - [[nodiscard]] const T &getAddr() const override { + [[nodiscard]] auto getAddr() const -> const T & override { return addr; } - [[nodiscard]] const std::string &toString() const override { + [[nodiscard]] auto toString() const -> const std::string & override { return str; } private: diff --git a/include/CppSockets/Certificate.hpp b/include/CppSockets/Certificate.hpp new file mode 100644 index 0000000..b39939f --- /dev/null +++ b/include/CppSockets/Certificate.hpp @@ -0,0 +1,212 @@ +/* +** Project LibCppSockets, 2025 +** +** Author Francois Michaut +** +** Started on Fri Aug 1 09:50:33 2025 Francois Michaut +** Last update Mon Aug 4 23:45:31 2025 Francois Michaut +** +** Certificate.hpp : Classes to create and manage Certificates +*/ + +#pragma once + +#include "CppSockets/SSL_Utils.hpp" + +#include + +#include +#include + +#if __cplusplus < 202002L +namespace std { + using u8string = basic_string; // NOLINT(cert-dcl58-cpp) +} +#endif + +namespace CppSockets { + class x509NameEntry; + + class x509Name { + public: + x509Name(); + x509Name(X509_NAME_ptr ptr); + + x509Name(const x509Name &other) { *this = other; } + x509Name(x509Name &&other) noexcept = default; + auto operator=(const x509Name &other) -> x509Name &; + auto operator=(x509Name &&other) noexcept -> x509Name & = default; + + ~x509Name() = default; + + [[nodiscard]] + auto clone() const -> x509Name { return {*this}; } + + void add_entry(const x509NameEntry &entry, int loc = -1, int set = 0); + void add_entry(const std::string &field_name, int type, const std::u8string &data, int loc = -1, int set = 0); + void add_entry(const ASN1_OBJECT *obj, int type, const std::u8string &data, int loc = -1, int set = 0); + void add_entry(int nid, int type, const std::u8string &data, int loc = -1, int set = 0); + + [[nodiscard]] + auto entry_count() const -> int; + [[nodiscard]] + auto get_entry(int loc) const -> x509NameEntry; + auto delete_entry(int loc) -> x509NameEntry; + + [[nodiscard]] + auto get_index(int nid, int lastpos = -1) const -> int; + [[nodiscard]] + auto get_index(const ASN1_OBJECT *obj, int lastpos = -1) const -> int; + + // TODO ? + // X509_NAME_get0_der + // X509_NAME_cmp + // X509_NAME_digest + // X509_NAME_hash + // X509_NAME_hash_ex + // X509_NAME_oneline + // X509_NAME_print + // X509_NAME_print_ex + // X509_NAME_print_ex_fp + + [[nodiscard]] + auto get() const -> X509_NAME * { return m_ptr.get(); } + private: + X509_NAME_ptr m_ptr; + }; + + class x509NameEntry { + public: + x509NameEntry(); + x509NameEntry(X509_NAME_ENTRY_ptr ptr); + x509NameEntry(const std::string &name, int type, const std::u8string &data); + x509NameEntry(const ASN1_OBJECT *obj, int type, const std::u8string &data); + x509NameEntry(int nid, int type, const std::u8string &data); + + x509NameEntry(const x509NameEntry &other) { *this = other; } + x509NameEntry(x509NameEntry &&other) noexcept = default; + auto operator=(const x509NameEntry &other) -> x509NameEntry &; + auto operator=(x509NameEntry &&other) noexcept -> x509NameEntry & = default; + + ~x509NameEntry() = default; + + [[nodiscard]] + auto clone() const -> x509NameEntry { return {*this}; } + + void set_object(const ASN1_OBJECT *obj); + void set_data(int type, const std::u8string &data); + + [[nodiscard]] + auto get_object() const -> ASN1_OBJECT *; + [[nodiscard]] + auto get_data() const -> ASN1_STRING *; + + [[nodiscard]] + auto get() const -> X509_NAME_ENTRY * { return m_ptr.get(); } + private: + X509_NAME_ENTRY_ptr m_ptr; + }; + + class x509Extension { + public: + x509Extension(); + x509Extension(X509_EXTENSION_ptr ptr); + x509Extension(int nid, int crit, ASN1_OCTET_STRING *data); + x509Extension(const ASN1_OBJECT *obj, int crit, ASN1_OCTET_STRING *data); + + x509Extension(const x509Extension &other) { *this = other; } + x509Extension(x509Extension &&other) noexcept = default; + auto operator=(const x509Extension &other) -> x509Extension &; + auto operator=(x509Extension &&other) noexcept -> x509Extension & = default; + + ~x509Extension() = default; + + [[nodiscard]] + auto clone() const -> x509Extension { return {*this}; } + + void set_data(ASN1_OCTET_STRING *data); + void set_object(const ASN1_OBJECT *obj); + void set_critical(bool crit); + + [[nodiscard]] + auto get_data() const -> ASN1_OCTET_STRING *; + [[nodiscard]] + auto get_object() const -> ASN1_OBJECT *; + [[nodiscard]] + auto get_critical() const -> bool; + + [[nodiscard]] + auto get() const -> X509_EXTENSION * { return m_ptr.get(); } + private: + X509_EXTENSION_ptr m_ptr; + }; + + class x509Certificate { + public: + x509Certificate(); + x509Certificate(X509_ptr x509); + explicit x509Certificate(const std::filesystem::path &pem_file_path); + + x509Certificate(const x509Certificate &other) { *this = other; } + x509Certificate(x509Certificate &&other) noexcept = default; + auto operator=(const x509Certificate &other) -> x509Certificate &; + auto operator=(x509Certificate &&other) noexcept -> x509Certificate & = default; + + ~x509Certificate() = default; + + [[nodiscard]] + auto clone() const -> x509Certificate { return {*this}; } + + // TODO: Provide overloads for password protected certs + void load(const std::filesystem::path &pem_file_path); + void save(const std::filesystem::path &pem_file_path) const; + + // TODO: Get methods + + // TODO: Provide overloads for hardcoded time + void set_not_before(int offset_day, std::int64_t offset_sec, time_t *in_tm); + void set_not_after(int offset_day, std::int64_t offset_sec, time_t *in_tm); + + void set_version(std::int64_t version); + [[nodiscard]] + auto get_version() const -> std::int64_t; + + void set_serial_number(std::int64_t serial_number); + void set_serial_number(std::uint64_t serial_number); + void set_serial_number(BIGNUM *serial_number); + + void set_public_key(const EVP_PKEY_ptr &pkey); + + void set_subject_name(const x509Name &name); + void set_issuer_name(const x509Name &name); + void set_self_signed_name(const x509Name &name); + + void add_extension(const x509Extension &ext, int loc = -1); + auto delete_extension(int loc) -> x509Extension; + [[nodiscard]] + auto extension_count() const -> int; + + [[nodiscard]] + auto get_extension(std::uint32_t loc) const -> x509Extension; + [[nodiscard]] + auto get_extension_by(int nid, int lastpos = - 1) const -> x509Extension; + [[nodiscard]] + auto get_extension_by(const ASN1_OBJECT *obj, int lastpos = -1) const -> x509Extension; + [[nodiscard]] + auto get_extension_by(bool critical, int lastpos = - 1) const -> x509Extension; + + [[nodiscard]] + auto self_signed(bool verify_signature) const -> bool; + [[nodiscard]] + auto verify(const EVP_PKEY_ptr &pkey) const -> bool; + + void sign(const EVP_PKEY_ptr &pkey, const EVP_MD *digest = EVP_sha256()); + + [[nodiscard]] + auto get() const -> X509 * { return m_ptr.get(); } + private: + X509_ptr m_ptr; + }; + + using Certificate = x509Certificate; +} diff --git a/include/CppSockets/IPv4.hpp b/include/CppSockets/IPv4.hpp index 3b487b3..99ccc38 100644 --- a/include/CppSockets/IPv4.hpp +++ b/include/CppSockets/IPv4.hpp @@ -20,9 +20,9 @@ namespace CppSockets { IPv4(const char *addr); // TODO add support for string. Maybe string_view ? - [[nodiscard]] std::uint32_t getAddress() const override; - [[nodiscard]] int getFamily() const override; - [[nodiscard]] const std::string &toString() const override; + [[nodiscard]] auto getAddress() const -> std::uint32_t override; + [[nodiscard]] auto getFamily() const -> int override; + [[nodiscard]] auto toString() const -> const std::string & override; private: std::uint32_t addr; diff --git a/include/CppSockets/SSL_Utils.hpp b/include/CppSockets/SSL_Utils.hpp new file mode 100644 index 0000000..3988d9b --- /dev/null +++ b/include/CppSockets/SSL_Utils.hpp @@ -0,0 +1,53 @@ +/* +** Project LibCppSockets, 2025 +** +** Author Francois Michaut +** +** Started on Fri Aug 1 09:54:53 2025 Francois Michaut +** Last update Sun Aug 3 23:32:20 2025 Francois Michaut +** +** SSL_Utils.hpp : SSL Utility types +*/ + +#pragma once + +#include + +#include + +#define CPP_SOCKETS_SSL_UTILS_DEFINE_DTOR(TYPE) \ + struct TYPE##_dtor { \ + void operator()(TYPE *ptr) { TYPE##_free(ptr); } \ + }; + +#define CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(TYPE) \ + CPP_SOCKETS_SSL_UTILS_DEFINE_DTOR(TYPE) \ + using TYPE##_ptr = std::unique_ptr; + +namespace CppSockets { + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(BIO) + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(SSL_CTX) + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(SSL) + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(X509) + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(X509_NAME) + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(X509_NAME_ENTRY) + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(X509_EXTENSION) + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(EVP_PKEY) + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(EVP_MD) + CPP_SOCKETS_SSL_UTILS_DEFINE_PTR(EVP_MD_CTX) + + void throw_openssl_error(); + auto check_or_throw_openssl_error(int ret) -> int; + + template + auto check_or_throw_openssl_error(T *ret) -> T * { + if (ret == nullptr) { + throw_openssl_error(); + } + return ret; + } +} + +// Don't leak macros +#undef CPP_SOCKETS_SSL_UTILS_DEFINE_DTOR +#undef CPP_SOCKETS_SSL_UTILS_DEFINE_PTR diff --git a/include/CppSockets/Socket.hpp b/include/CppSockets/Socket.hpp index 973ec36..7ff7ba8 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 Sat Dec 9 08:55:07 2023 Francois Michaut +** Last update Tue Aug 5 00:00:48 2025 Francois Michaut ** ** Socket.hpp : Portable C++ socket class */ @@ -42,51 +42,53 @@ namespace CppSockets { // TODO Maybe enable copy with dup(2) ? Socket(const Socket &other) = delete; Socket(Socket &&other) noexcept; - Socket &operator=(const Socket &other) = delete; - Socket &operator=(Socket &&other) noexcept; + auto operator=(const Socket &other) -> Socket & = delete; + auto operator=(Socket &&other) noexcept -> Socket &; - std::string read(std::size_t len = -1); - std::size_t read(char *buff, std::size_t size); - std::size_t write(const std::string &buff); - std::size_t write(const char *buff, std::size_t len); + 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; - int set_reuseaddr(bool value); - int getsockopt(int level, int optname, SockOptType *optval, socklen_t *optlen); - int setsockopt(int level, int optname, const SockOptType *optval, socklen_t optlen); + 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(); - int connect(const IEndpoint &endpoint); - int connect(const std::string &addr, uint16_t port); + auto connect(const IEndpoint &endpoint) -> int; + auto connect(const std::string &addr, uint16_t port) -> int; - int bind(const IEndpoint &endpoint); - int bind(const std::string &addr, uint16_t port); - int listen(int backlog); - std::shared_ptr accept(void *addr_out = nullptr); + 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; void set_blocking(bool val); [[nodiscard]] - RawSocketType get_fd() const; // TODO check if windows SOCKET can be - // converted to int + auto get_fd() const -> RawSocketType { return m_sockfd; } [[nodiscard]] - int get_type() const; + auto get_type() const -> int { return m_type; } [[nodiscard]] - int get_domain() const; + auto get_domain() const -> int { return m_domain; } [[nodiscard]] - int get_protocol() const; + auto get_protocol() const -> int { return m_protocol; } + // TODO: Allow to get Endpoint [[nodiscard]] - bool connected() const; + auto connected() const -> bool { return m_is_connected; } - public: - static int get_errno(); - static char *strerror(int err); - static char *strerror(); + static auto get_errno() -> int; + static auto strerror(int err) -> char *; + static auto strerror() -> char *; protected: - static int getsockopt(int fd, int level, int optname, SockOptType *optval, socklen_t *optlen); - int bind(std::uint32_t addr, uint16_t port); + static auto getsockopt(int fd, int level, int optname, SockOptType *optval, socklen_t *optlen) -> int; + auto bind(std::uint32_t addr, uint16_t port) -> int; + + void set_connected(bool status) { m_is_connected = status; }; + private: int m_domain = 0; int m_type = 0; int m_protocol = 0; diff --git a/include/CppSockets/TlsSocket.hpp b/include/CppSockets/TlsSocket.hpp index 1befc19..c0d322e 100644 --- a/include/CppSockets/TlsSocket.hpp +++ b/include/CppSockets/TlsSocket.hpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Sep 14 20:51:23 2022 Francois Michaut -** Last update Tue Nov 14 19:37:45 2023 Francois Michaut +** Last update Tue Aug 5 00:01:16 2025 Francois Michaut ** ** SecureSocket.hpp : TLS socket wrapper using openssl */ @@ -13,66 +13,48 @@ #include "CppSockets/OSDetection.hpp" -#include "CppSockets/IPv4.hpp" +#include "CppSockets/SSL_Utils.hpp" #include "CppSockets/Socket.hpp" -#include - -// TODO: find a better way do to this -using BIO = struct bio_st; -using SSL = struct ssl_st; -using SSL_METHOD = struct ssl_method_st; -using SSL_CTX = struct ssl_ctx_st; -using X509 = struct x509_st; -using RSA = struct rsa_st; -using EVP_PKEY = struct evp_pkey_st; -using EVP_MD = struct evp_md_st; -using EVP_MD_CTX = struct evp_md_ctx_st; - namespace CppSockets { - using BIO_ptr=std::unique_ptr>; - using SSL_CTX_ptr=std::unique_ptr>; - using SSL_ptr=std::unique_ptr>; - using X509_ptr=std::unique_ptr>; - using RSA_ptr=std::unique_ptr>; - using EVP_PKEY_ptr=std::unique_ptr>; - using EVP_MD_ptr=std::unique_ptr>; - using EVP_MD_CTX_ptr=std::unique_ptr>; - // 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(int domain, int type, int protocol); - TlsSocket(Socket &&other, SSL_ptr ssl = nullptr); - TlsSocket(RawSocketType fd, SSL_ptr ssl = nullptr); - ~TlsSocket(); + explicit TlsSocket(Socket &&other, SSL_ptr ssl = nullptr); + explicit TlsSocket(RawSocketType fd, SSL_ptr ssl = nullptr); + ~TlsSocket() noexcept; TlsSocket(const TlsSocket &other) = delete; TlsSocket(TlsSocket &&other) noexcept; - TlsSocket &operator=(const TlsSocket &other) = delete; - TlsSocket &operator=(TlsSocket &&other) noexcept; + auto operator=(const TlsSocket &other) -> TlsSocket & = delete; + auto operator=(TlsSocket &&other) noexcept -> TlsSocket &; - std::string read(std::size_t len = -1); - std::size_t read(char *buff, std::size_t size); - std::size_t write(const std::string &buff); - std::size_t write(std::string_view buff); - std::size_t write(const char *buff, std::size_t len); + 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 { return this->write(buff.c_str(), buff.size()); } + 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; - void set_certificate(std::string cert_path, std::string pkey_path); - int connect(const IEndpoint &endpoint); + void set_verify(int mode, SSL_verify_cb verify_callback = nullptr); + void set_certificate(const std::string &cert_path, const std::string &pkey_path); + auto connect(const IEndpoint &endpoint) -> int; - std::shared_ptr accept(void *addr_out = nullptr); + auto accept(void *addr_out = nullptr, const SSL_CTX_ptr &ctx = nullptr) -> std::unique_ptr; + auto accept(const SSL_CTX_ptr &ctx) -> std::unique_ptr; [[nodiscard]] - const SSL_CTX_ptr &get_ssl_ctx() const; + auto get_ssl_ctx() const -> const SSL_CTX_ptr & { return m_ctx; } [[nodiscard]] - const SSL_ptr &get_ssl() const; + auto get_ssl() const -> const SSL_ptr & { return m_ssl; } [[nodiscard]] - const X509_ptr &get_client_cert() const; + auto get_client_cert() const -> const X509_ptr & { return m_peer_cert; } [[nodiscard]] - const std::string tls_strerror(int ret); + auto tls_strerror(int ret) -> std::string; private: SSL_CTX_ptr m_ctx; SSL_ptr m_ssl; @@ -80,26 +62,6 @@ namespace CppSockets { X509_ptr m_cert; EVP_PKEY_ptr m_pkey; - void check_for_error(std::string error_msg, int ret); + void check_for_error(const std::string &error_msg, int ret); }; - - inline std::size_t TlsSocket::write(std::string_view buff) { - return write(buff.data(), buff.size()); - } - - inline std::size_t TlsSocket::write(const std::string &buff) { - return write(buff.c_str(), buff.size()); - } - - inline const SSL_CTX_ptr &TlsSocket::get_ssl_ctx() const { - return m_ctx; - } - - inline const SSL_ptr &TlsSocket::get_ssl() const { - return m_ssl; - } - - inline const X509_ptr &TlsSocket::get_client_cert() const { - return m_peer_cert; - } } diff --git a/source/Address.cpp b/source/Address.cpp index ea48c00..76a26d1 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 Mon Aug 29 20:45:51 2022 Francois Michaut +** Last update Sun Aug 3 21:59:36 2025 Francois Michaut ** ** Address.cpp : Implementation of generic Address classes & functions */ @@ -12,7 +12,7 @@ #include "CppSockets/Address.hpp" namespace CppSockets { - std::string IEndpoint::makeString() const { + auto IEndpoint::makeString() const -> std::string { return this->getAddr().toString() + ":" + std::to_string(this->getPort()); } } diff --git a/source/Certificate.cpp b/source/Certificate.cpp new file mode 100644 index 0000000..b5de03e --- /dev/null +++ b/source/Certificate.cpp @@ -0,0 +1,430 @@ +/* +** Project LibCppSockets, 2025 +** +** Author Francois Michaut +** +** Started on Sat Aug 2 22:41:35 2025 Francois Michaut +** Last update Tue Aug 5 13:08:40 2025 Francois Michaut +** +** Certificate.cpp : Implementation of classes to create and manage Certificates +*/ + +#include "CppSockets/Certificate.hpp" +#include "CppSockets/SSL_Utils.hpp" + +#include +#include +#include +#include + +#define REQUIRED_PTR(ptr, name) \ + if (!ptr) { \ + throw std::runtime_error("Failed to create " name); \ + } + +#define ASSIGNMENT_OPERATOR(type) \ + if (this == &other) { \ + return *this; \ + } \ + \ + type *dup = type##_dup(other.m_ptr.get()); \ + \ + if (dup == nullptr) { \ + throw std::runtime_error("Failed to dup ##type##"); \ + } \ + this->m_ptr.reset(dup); \ + return *this; \ + +namespace { + template + inline auto numeric_cast(const Src value) -> Dst { + const Dst result = static_cast(value); + + if (result != value) { + throw std::overflow_error("Overflow/Underflow error"); + } + return result; + } +} + +// x509Name +namespace CppSockets { + x509Name::x509Name() : + m_ptr(X509_NAME_new()) + { + REQUIRED_PTR(m_ptr, "X509_NAME") + } + + x509Name::x509Name(X509_NAME_ptr ptr) : + m_ptr(std::move(ptr)) + { + REQUIRED_PTR(m_ptr, "X509_NAME") + } + + auto x509Name::operator=(const x509Name &other) -> x509Name & { + ASSIGNMENT_OPERATOR(X509_NAME) + } + + void x509Name::add_entry(const x509NameEntry &entry, int loc, int set) { + auto ret = X509_NAME_add_entry(m_ptr.get(), entry.get(), loc, set); + + check_or_throw_openssl_error(ret); + } + + void x509Name::add_entry(const std::string &field_name, int type, const std::u8string &data, int loc, int set) { + auto ret = X509_NAME_add_entry_by_txt(m_ptr.get(), field_name.c_str(), type, data.c_str(), numeric_cast(data.size()), loc, set); + + check_or_throw_openssl_error(ret); + } + + void x509Name::add_entry(const ASN1_OBJECT *obj, int type, const std::u8string &data, int loc, int set) { + auto ret = X509_NAME_add_entry_by_OBJ(m_ptr.get(), obj, type, data.c_str(), numeric_cast(data.size()), loc, set); + + check_or_throw_openssl_error(ret); + } + + void x509Name::add_entry(int nid, int type, const std::u8string &data, int loc, int set) { + auto ret = X509_NAME_add_entry_by_NID(m_ptr.get(), nid, type, data.c_str(), numeric_cast(data.size()), loc, set); + + check_or_throw_openssl_error(ret); + } + + auto x509Name::entry_count() const -> int { + return X509_NAME_entry_count(m_ptr.get()); + } + + auto x509Name::get_entry(int loc) const -> x509NameEntry { + X509_NAME_ENTRY_ptr ptr{check_or_throw_openssl_error(X509_NAME_get_entry(m_ptr.get(), loc))}; + + return {std::move(ptr)}; + } + + auto x509Name::delete_entry(int loc) -> x509NameEntry { + X509_NAME_ENTRY_ptr ptr{check_or_throw_openssl_error(X509_NAME_delete_entry(m_ptr.get(), loc))}; + + return {std::move(ptr)}; + } + + auto x509Name::get_index(int nid, int lastpos) const -> int { + return X509_NAME_get_index_by_NID(m_ptr.get(), nid, lastpos); + } + + auto x509Name::get_index(const ASN1_OBJECT *obj, int lastpos) const -> int { + return X509_NAME_get_index_by_OBJ(m_ptr.get(), obj, lastpos); + } +} + +// x509NameEntry +namespace CppSockets { + x509NameEntry::x509NameEntry() : + m_ptr(X509_NAME_ENTRY_new()) + { + REQUIRED_PTR(m_ptr, "X509_NAME_ENTRY") + } + + x509NameEntry::x509NameEntry(X509_NAME_ENTRY_ptr ptr) : + m_ptr(std::move(ptr)) + { + REQUIRED_PTR(m_ptr, "X509_NAME_ENTRY") + } + + x509NameEntry::x509NameEntry(const std::string &name, int type, const std::u8string &data) : + m_ptr(X509_NAME_ENTRY_create_by_txt(nullptr, name.c_str(), type, data.c_str(), numeric_cast(data.size()))) + { + REQUIRED_PTR(m_ptr, "X509_NAME_ENTRY") + } + + x509NameEntry::x509NameEntry(const ASN1_OBJECT *obj, int type, const std::u8string &data) : + m_ptr(X509_NAME_ENTRY_create_by_OBJ(nullptr, obj, type, data.c_str(), numeric_cast(data.size()))) + { + REQUIRED_PTR(m_ptr, "X509_NAME_ENTRY") + } + + x509NameEntry::x509NameEntry(int nid, int type, const std::u8string &data) : + m_ptr(X509_NAME_ENTRY_create_by_NID(nullptr, nid, type, data.c_str(), numeric_cast(data.size()))) + { + REQUIRED_PTR(m_ptr, "X509_NAME_ENTRY") + } + + auto x509NameEntry::operator=(const x509NameEntry &other) -> x509NameEntry & { + ASSIGNMENT_OPERATOR(X509_NAME_ENTRY) + } + + void x509NameEntry::set_object(const ASN1_OBJECT *obj) { + auto ret = X509_NAME_ENTRY_set_object(m_ptr.get(), obj); + + check_or_throw_openssl_error(ret); + } + + void x509NameEntry::set_data(int type, const std::u8string &data) { + auto ret = X509_NAME_ENTRY_set_data(m_ptr.get(), type, data.c_str(), numeric_cast(data.size())); + + check_or_throw_openssl_error(ret); + } + + auto x509NameEntry::get_object() const -> ASN1_OBJECT * { + return check_or_throw_openssl_error(X509_NAME_ENTRY_get_object(m_ptr.get())); + } + + auto x509NameEntry::get_data() const -> ASN1_STRING * { + return check_or_throw_openssl_error(X509_NAME_ENTRY_get_data(m_ptr.get())); + } +} + +// x509Extension +namespace CppSockets { + x509Extension::x509Extension() : + m_ptr(X509_EXTENSION_new()) + { + REQUIRED_PTR(m_ptr, "X509_EXTENSION") + } + + x509Extension::x509Extension(X509_EXTENSION_ptr ptr) : + m_ptr(std::move(ptr)) + { + REQUIRED_PTR(m_ptr, "X509_EXTENSION") + } + + x509Extension::x509Extension(int nid, int crit, ASN1_OCTET_STRING *data) : + m_ptr(X509_EXTENSION_create_by_NID(nullptr, nid, crit, data)) + { + REQUIRED_PTR(m_ptr, "X509_EXTENSION") + } + + x509Extension::x509Extension(const ASN1_OBJECT *obj, int crit, ASN1_OCTET_STRING *data) : + m_ptr(X509_EXTENSION_create_by_OBJ(nullptr, obj, crit, data)) + { + REQUIRED_PTR(m_ptr, "X509_EXTENSION") + } + + auto x509Extension::operator=(const x509Extension &other) -> x509Extension & { + ASSIGNMENT_OPERATOR(X509_EXTENSION) + } + + void x509Extension::set_data(ASN1_OCTET_STRING *data) { + check_or_throw_openssl_error(X509_EXTENSION_set_data(m_ptr.get(), data)); + } + + void x509Extension::set_object(const ASN1_OBJECT *obj) { + check_or_throw_openssl_error(X509_EXTENSION_set_object(m_ptr.get(), obj)); + } + + void x509Extension::set_critical(bool crit) { + check_or_throw_openssl_error(X509_EXTENSION_set_critical(m_ptr.get(), crit)); + } + + auto x509Extension::get_data() const -> ASN1_OCTET_STRING * { + return X509_EXTENSION_get_data(m_ptr.get()); + } + + auto x509Extension::get_object() const -> ASN1_OBJECT * { + return X509_EXTENSION_get_object(m_ptr.get()); + } + + auto x509Extension::get_critical() const -> bool { + return X509_EXTENSION_get_critical(m_ptr.get()) == 1; + } +} + +// x509Certificate +namespace CppSockets { + x509Certificate::x509Certificate() : + m_ptr(X509_new()) + { + REQUIRED_PTR(m_ptr, "X509") + } + + x509Certificate::x509Certificate(X509_ptr x509) : + m_ptr(std::move(x509)) + { + REQUIRED_PTR(m_ptr, "X509") + } + + x509Certificate::x509Certificate(const std::filesystem::path &pem_file_path) { + load(pem_file_path); + } + + auto x509Certificate::operator=(const x509Certificate &other) -> x509Certificate & { + ASSIGNMENT_OPERATOR(X509) + } + + void x509Certificate::load(const std::filesystem::path &pem_file_path) { + BIO_ptr bio(BIO_new_file(pem_file_path.string().c_str(), "r")); + + if (!bio) { + throw std::runtime_error("Failed to open file"); + } + auto *x509 = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr); + + if (!x509) { + throw std::runtime_error("Failed to load X509 Certificate"); + } + m_ptr.reset(x509); + } + + void x509Certificate::save(const std::filesystem::path &pem_file_path) const { + BIO_ptr bio(BIO_new_file(pem_file_path.string().c_str(), "w")); + + if (!bio) { + throw std::runtime_error("Failed to open file"); + } + if (PEM_write_bio_X509(bio.get(), m_ptr.get()) < 1) { + throw std::runtime_error("Failed to save X509 Certificate"); + } + } + + 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)) { + throw std::runtime_error("Failed to adjust not_before time"); + } + } + + 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)) { + 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)) { + throw std::runtime_error("Failed to set version"); + } + } + + auto x509Certificate::get_version() const -> std::int64_t { + return X509_get_version(m_ptr.get()); + } + + void x509Certificate::set_serial_number(int64_t serial_number) { + ASN1_INTEGER *ptr = X509_get_serialNumber(m_ptr.get()); + + if (!ASN1_INTEGER_set_int64(ptr, serial_number)) { + throw std::runtime_error("Failed to set serial number"); + } + } + + void x509Certificate::set_serial_number(uint64_t serial_number) { + ASN1_INTEGER *ptr = X509_get_serialNumber(m_ptr.get()); + + if (!ASN1_INTEGER_set_uint64(ptr, serial_number)) { + throw std::runtime_error("Failed to set serial number"); + } + } + + void x509Certificate::set_serial_number(BIGNUM *serial_number) { + ASN1_INTEGER *ptr = X509_get_serialNumber(m_ptr.get()); + + if (!BN_to_ASN1_INTEGER(serial_number, ptr)) { + throw std::runtime_error("Failed to set serial number"); + } + } + + void x509Certificate::set_public_key(const EVP_PKEY_ptr &pkey) { + if (!X509_set_pubkey(m_ptr.get(), pkey.get())) { + throw std::runtime_error("Failed to set public key"); + } + } + + void x509Certificate::set_subject_name(const x509Name &name) { + if (!X509_set_subject_name(m_ptr.get(), name.get())) { + throw std::runtime_error("Failed to set subject name"); + } + } + + void x509Certificate::set_issuer_name(const x509Name &name) { + if (!X509_set_issuer_name(m_ptr.get(), name.get())) { + throw std::runtime_error("Failed to set issuer name"); + } + } + + void x509Certificate::set_self_signed_name(const x509Name &name) { + set_issuer_name(name); + set_subject_name(name); + } + + void x509Certificate::add_extension(const x509Extension &ext, int loc) { + if (!X509_add_ext(m_ptr.get(), ext.get(), loc)) { + throw std::runtime_error("Failed to add extension"); + } + } + + auto x509Certificate::delete_extension(int loc) -> x509Extension { + X509_EXTENSION_ptr ptr {X509_delete_ext(m_ptr.get(), loc)}; + + if (!ptr) { + throw std::runtime_error("Failed to delete extension"); + } + return {std::move(ptr)}; + } + + auto x509Certificate::extension_count() const -> int { + return X509_get_ext_count(m_ptr.get()); + } + + auto x509Certificate::get_extension(std::uint32_t loc) const -> x509Extension { + X509_EXTENSION_ptr ptr {X509_get_ext(m_ptr.get(), numeric_cast(loc))}; + + if (!ptr) { + throw std::runtime_error("Failed to get extension"); + } + return {std::move(ptr)}; + } + + auto x509Certificate::get_extension_by(int nid, int lastpos) const -> x509Extension { + int idx = X509_get_ext_by_NID(m_ptr.get(), nid, lastpos); + + if (idx < 0) { + throw std::runtime_error("Failed to get extension"); + } + return get_extension(idx); + } + + auto x509Certificate::get_extension_by(const ASN1_OBJECT *obj, int lastpos) const -> x509Extension { + int idx = X509_get_ext_by_OBJ(m_ptr.get(), obj, lastpos); + + if (idx < 0) { + throw std::runtime_error("Failed to get extension"); + } + return get_extension(idx); + } + + auto x509Certificate::get_extension_by(bool critical, int lastpos) const -> x509Extension { + int idx = X509_get_ext_by_critical(m_ptr.get(), critical, lastpos); + + if (idx < 0) { + throw std::runtime_error("Failed to get extension"); + } + return get_extension(idx); + } + + auto x509Certificate::self_signed(bool verify_signature) const -> bool { + auto ret = X509_self_signed(m_ptr.get(), verify_signature); + + if (ret < 0) { + throw std::runtime_error("Failed to check certificate self-signed"); + } + return ret; + } + + auto x509Certificate::verify(const EVP_PKEY_ptr &pkey) const -> bool { + auto ret = X509_verify(m_ptr.get(), pkey.get()); + + if (ret < 0) { + throw std::runtime_error("Failed to check certificate signature"); + } + return ret; + } + + void x509Certificate::sign(const EVP_PKEY_ptr &pkey, const EVP_MD *digest) { + auto ret = X509_sign(m_ptr.get(), pkey.get(), digest); + + if (ret < 0) { + throw std::runtime_error("Failed to sign certificate"); + } + } +} diff --git a/source/IPv4.cpp b/source/IPv4.cpp index 4597286..45a0dbf 100644 --- a/source/IPv4.cpp +++ b/source/IPv4.cpp @@ -4,13 +4,13 @@ ** Author Francois Michaut ** ** Started on Sun Feb 13 18:52:28 2022 Francois Michaut -** Last update Sat Dec 2 16:17:43 2023 Francois Michaut +** Last update Tue Aug 5 01:43:47 2025 Francois Michaut ** ** IPv4.cpp : Implementation of IPv4 class */ +#include "CppSockets/OSDetection.hpp" #include "CppSockets/IPv4.hpp" -#include "CppSockets/Socket.hpp" #include #include @@ -34,22 +34,22 @@ namespace CppSockets { IPv4::IPv4(const char *addr) : str(addr) { - struct in_addr in; + struct in_addr address = {}; - if (inet_pton(AF_INET, addr, &in) != 1) + if (inet_pton(AF_INET, addr, &address) != 1) throw std::runtime_error("Invalid IPv4 address"); - this->addr = in.s_addr; + this->addr = address.s_addr; } - std::uint32_t IPv4::getAddress() const { + auto IPv4::getAddress() const -> std::uint32_t { return addr; } - const std::string &IPv4::toString() const { + auto IPv4::toString() const -> const std::string & { return str; } - int IPv4::getFamily() const { + auto IPv4::getFamily() const -> int { return AF_INET; } } diff --git a/source/SSL_Utils.cpp b/source/SSL_Utils.cpp new file mode 100644 index 0000000..ac42fa9 --- /dev/null +++ b/source/SSL_Utils.cpp @@ -0,0 +1,46 @@ +/* +** Project LibFileShareProtocol, 2025 +** +** Author Francois Michaut +** +** Started on Sun Aug 3 20:36:03 2025 Francois Michaut +** Last update Sun Aug 3 22:12:04 2025 Francois Michaut +** +** SSL_Utils.cpp : SSL Utility implementations +*/ + +#include "CppSockets/SSL_Utils.hpp" + +#include + +#include +#include +#include + +const auto SSL_MAX_ERROR = 256; + +namespace CppSockets { + // TODO: Double check the usage of theses functions : + // - Is there any place where they are used, where the function called doesn't push an error for ERR_get_error + // - Is there any place where they should be used, but we are throwing a manual exception ? + + // TODO: Use a custom exception instead of runtime error + void throw_openssl_error() { + auto error = ERR_get_error(); + std::array buff = {0}; + + if (error == 0) + return; + + ERR_error_string_n(error, buff.data(), buff.size()); + + throw std::runtime_error(std::string(buff.data())); + } + + auto check_or_throw_openssl_error(int ret) -> int { + if (ret == 0) { + throw_openssl_error(); + } + return ret; + } +} diff --git a/source/Socket.cpp b/source/Socket.cpp index 63452fb..41a6a90 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 Sat Dec 2 17:11:28 2023 Francois Michaut +** Last update Tue Aug 5 00:04:25 2025 Francois Michaut ** ** Socket.cpp : Protable C++ socket class implementation */ @@ -14,12 +14,12 @@ #include "CppSockets/Socket.hpp" #ifdef OS_WINDOWS - #include #include + #include #else + #include #include #include - #include #include // To match windows's constants @@ -71,7 +71,11 @@ namespace CppSockets { *this = std::move(other); } - Socket &Socket::operator=(Socket &&other) noexcept { + Socket::~Socket() { + close(); + } + + auto Socket::operator=(Socket &&other) noexcept -> Socket & { if (&other == this) return *this; this->close(); @@ -83,7 +87,7 @@ namespace CppSockets { return *this; } - void Socket::close() { + void Socket::close() { // NOLINT(readability-make-member-function-const) if (m_sockfd != INVALID_SOCKET) { #ifdef OS_WINDOWS closesocket(m_sockfd); @@ -93,28 +97,23 @@ namespace CppSockets { } } - Socket::~Socket() { - close(); - } - - int Socket::getsockopt(int fd, int level, int optname, SockOptType *optval, socklen_t *optlen) { - int ret = ::getsockopt(fd, level, optname, optval, optlen); + 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) { + if (ret == SOCKET_ERROR) throw std::runtime_error(std::string("Failed to get sock opt: ") + Socket::strerror()); - } return ret; } - char *Socket::strerror() { + auto Socket::strerror() -> char * { return Socket::strerror(Socket::get_errno()); } - char *Socket::strerror(int err) { + auto Socket::strerror(int err) -> char * { return ::strerror(err); } - int Socket::get_errno() { + auto Socket::get_errno() -> int { #ifdef OS_WINDOWS return WSAGetLastError(); #else @@ -122,8 +121,8 @@ namespace CppSockets { #endif } - std::string Socket::read(std::size_t len) { - std::array buff = {0}; + 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::size_t nb = 1; @@ -137,126 +136,115 @@ namespace CppSockets { return res.str(); } - std::size_t Socket::read(char *buff, std::size_t size) { - std::size_t ret; + auto Socket::read(char *buff, std::size_t size) -> std::size_t { + std::ptrdiff_t ret; if (!m_is_connected) throw std::runtime_error("Not connected"); ret = ::read(m_sockfd, buff, size); - if (ret < 0) { + if (ret < 0) throw std::runtime_error(std::string("Failed to read from socket: ") + Socket::strerror()); - } else if (ret == 0 && size > 0) { + if (ret == 0 && size > 0) { m_is_connected = false; } return ret; } - std::size_t Socket::write(const std::string &buff) { + auto Socket::write(const std::string &buff) -> std::size_t { return this->write(buff.data(), buff.size()); } - std::size_t Socket::write(const char *buff, std::size_t len) { - std::size_t ret; + auto Socket::write(const char *buff, std::size_t len) -> std::size_t { // NOLINT(readability-make-member-function-const) + std::ptrdiff_t ret; if (!m_is_connected) throw std::runtime_error("Not connected"); ret = ::write(m_sockfd, buff, len); - if (ret < 0) { + if (ret < 0) throw std::runtime_error(std::string("Failed to write to socket: ") + Socket::strerror()); - } return ret; } - int Socket::set_reuseaddr(bool value) { - int val = value; + auto Socket::set_reuseaddr(bool value) -> int { + int val = static_cast(value); return this->setsockopt(SOL_SOCKET, SO_REUSEADDR, (SockOptType *)&val, sizeof(val)); } - int Socket::getsockopt(int level, int optname, SockOptType *optval, socklen_t *optlen) { - return this->getsockopt(m_sockfd, level, optname, optval, optlen); + auto Socket::getsockopt(int level, int optname, SockOptType *optval, socklen_t *optlen) -> int { // NOLINT(readability-make-member-function-const) + return CppSockets::Socket::getsockopt(m_sockfd, level, optname, optval, optlen); } - int Socket::setsockopt(int level, int optname, const SockOptType *optval, socklen_t optlen) { + auto Socket::setsockopt(int level, int optname, const SockOptType *optval, socklen_t optlen) -> int { // NOLINT(readability-make-member-function-const) int ret = ::setsockopt(m_sockfd, level, optname, optval, optlen); - if (ret < 0) { + if (ret < 0) throw std::runtime_error(std::string("Failed to set sock opt: ") + Socket::strerror()); - } return ret; } - int Socket::bind(const std::string &addr, uint16_t port) { + auto Socket::bind(const std::string &addr, uint16_t port) -> int { return this->bind(inet_addr(addr.c_str()), port); } - int Socket::bind(const IEndpoint &endpoint) { - // TODO: this only works for IPv4. Need to switch getFamily() to handle IPv6 / AF_UNIX ... + auto Socket::bind(const IEndpoint &endpoint) -> int { + // TODO: this only works for IPv4. Need to switch getFamily() to handle + // IPv6 / AF_UNIX ... return this->bind(endpoint.getAddr().getAddress(), endpoint.getPort()); } - int Socket::bind(std::uint32_t source_addr, uint16_t 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, (struct sockaddr *)&addr, sizeof(addr)); - if (ret < 0) { + 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; } - int Socket::connect(const std::string &addr, uint16_t port) { + auto Socket::connect(const std::string &addr, uint16_t port) -> int { return this->connect(Endpoint(IPv4(addr.c_str()), port)); } - int Socket::connect(const IEndpoint &endpoint) { + auto Socket::connect(const IEndpoint &endpoint) -> int { struct sockaddr_in addr = {0}; int ret = 0; addr.sin_addr.s_addr = endpoint.getAddr().getAddress(); addr.sin_port = htons(endpoint.getPort()); addr.sin_family = endpoint.getAddr().getFamily(); - ret = ::connect(m_sockfd, (const struct sockaddr *)&addr, sizeof(addr)); - if (ret < 0) { + ret = ::connect(m_sockfd, reinterpret_cast(&addr), sizeof(addr)); + if (ret < 0) throw std::runtime_error(std::string("Failed to connect socket to ") + endpoint.toString() + " : " + Socket::strerror()); - } m_is_connected = ret == 0; return ret; } - bool Socket::connected() const { - return m_is_connected; - } - - int Socket::listen(int backlog) { + auto Socket::listen(int backlog) -> int { // NOLINT(readability-make-member-function-const) int ret = ::listen(m_sockfd, backlog); - if (ret < 0) { + if (ret < 0) throw std::runtime_error(std::string("Failed to listen socket: ") + Socket::strerror()); - } return ret; } - std::shared_ptr Socket::accept(void *addr_out) { + auto Socket::accept(void *addr_out) -> std::unique_ptr { // NOLINT(readability-make-member-function-const) int fd = ::accept(m_sockfd, nullptr, nullptr); - int domain = 0; - int type = 0; - int protocol = 0; if (addr_out != nullptr) { // TODO figure it out } - if (fd == INVALID_SOCKET) { + if (fd == INVALID_SOCKET) return nullptr; - } - return std::shared_ptr(new Socket(fd, true)); + return std::make_unique(fd, true); } - void Socket::set_blocking(bool val) { + void Socket::set_blocking(bool val) { // NOLINT(readability-make-member-function-const) #ifdef OS_WINDOWS u_long mode = val ? 0 : 1; int result = ioctlsocket(m_sockfd, FIONBIO, &mode); @@ -279,20 +267,4 @@ namespace CppSockets { } #endif } - - RawSocketType Socket::get_fd() const { - return m_sockfd; - } - - int Socket::get_domain() const { - return m_domain; - } - - int Socket::get_protocol() const { - return m_protocol; - } - - int Socket::get_type() const { - return m_type; - } } diff --git a/source/TlsSocket.cpp b/source/TlsSocket.cpp index 925a626..247af78 100644 --- a/source/TlsSocket.cpp +++ b/source/TlsSocket.cpp @@ -4,7 +4,7 @@ ** Author Francois Michaut ** ** Started on Wed Sep 14 21:04:42 2022 Francois Michaut -** Last update Sat Dec 2 11:33:00 2023 Francois Michaut +** Last update Sun Aug 3 22:18:06 2025 Francois Michaut ** ** SecureSocket.cpp : TLS socket wrapper implementation */ @@ -21,9 +21,22 @@ static constexpr int BUFF_SIZE = 4096; -// TODO use custom exceptions -namespace CppSockets { - static void init_ssl_socket(SSL *ssl, SSL_CTX *ctx, TlsSocket *socket) { +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, SSL_CTX *ctx, CppSockets::TlsSocket *socket) { int success = 1; if (!ctx || !ssl || !SSL_set_fd(ssl, socket->get_fd())) { @@ -35,16 +48,19 @@ namespace CppSockets { throw std::runtime_error(std::string("Failed to select TLS version: ") + socket->tls_strerror(0)); } } +} +// TODO use custom exceptions +namespace CppSockets { // TODO check if base destroctor is called (need to close the socket if error is raised) // TODO check if needs to call SSL_shutdown in such cases TlsSocket::TlsSocket(int domain, int type, int protocol) : CppSockets::Socket(domain, type, protocol), - m_ctx(SSL_CTX_new(TLS_method()), SSL_CTX_free), - m_ssl((m_ctx ? SSL_new(m_ctx.get()) : nullptr), SSL_free), - m_peer_cert(nullptr, X509_free), - m_cert(nullptr, X509_free), - m_pkey(nullptr, EVP_PKEY_free) + m_ctx(SSL_CTX_new(TLS_method())), + m_ssl((m_ctx ? SSL_new(m_ctx.get()) : nullptr)), + m_peer_cert(nullptr), + m_cert(nullptr), + m_pkey(nullptr) { init_ssl_socket(m_ssl.get(), m_ctx.get(), this); } @@ -62,22 +78,26 @@ 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_CTX_free), - m_ssl(ssl ? std::move(ssl) : (m_ctx ? SSL_ptr(SSL_new(m_ctx.get()), SSL_free) : nullptr)), - m_peer_cert(nullptr, X509_free), - m_cert(nullptr, X509_free), - m_pkey(nullptr, EVP_PKEY_free) + m_ctx((ssl ? SSL_get_SSL_CTX(ssl.get()) : SSL_CTX_new(TLS_method()))), + m_ssl(ssl ? std::move(ssl) : (m_ctx ? SSL_ptr(SSL_new(m_ctx.get())) : nullptr)), + m_peer_cert(nullptr), + m_cert(nullptr), + m_pkey(nullptr) { init_ssl_socket(m_ssl.get(), m_ctx.get(), this); } - TlsSocket::~TlsSocket() { - if (m_ssl && m_is_connected) { + TlsSocket::~TlsSocket() noexcept { + if (m_ssl && this->connected()) { int ret = SSL_shutdown(m_ssl.get()); // TODO: log failure if (ret == 0) { - while (m_is_connected) { - this->read(); + try { + while (this->connected()) { + this->read(); + } + } catch (std::runtime_error &e) { + // TODO: What ? } SSL_shutdown(m_ssl.get()); // TODO: log failure } @@ -90,8 +110,7 @@ namespace CppSockets { m_cert(std::move(other.m_cert)), m_pkey(std::move(other.m_pkey)) {} - TlsSocket &TlsSocket::operator=(TlsSocket &&other) noexcept - { + auto TlsSocket::operator=(TlsSocket &&other) noexcept -> TlsSocket & { m_ctx = std::move(other.m_ctx); m_ssl = std::move(other.m_ssl); m_peer_cert = std::move(other.m_peer_cert); @@ -102,13 +121,21 @@ namespace CppSockets { return *this; } - void TlsSocket::set_certificate(std::string cert_path, std::string pkey_path) { - BIO_ptr cert(BIO_new_file(cert_path.c_str(), "r"), BIO_free); - BIO_ptr pkey(BIO_new_file(pkey_path.c_str(), "r"), BIO_free); - // TODO: handle pkey password: SSL_CTX_set_default_passwd_cb - X509_ptr x509(PEM_read_bio_X509(cert.get(), nullptr, nullptr, nullptr), X509_free); - EVP_PKEY_ptr evp_pkey(PEM_read_bio_PrivateKey(pkey.get(), nullptr, nullptr, nullptr), EVP_PKEY_free); + 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_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")); + // TODO: handle pkey password: SSL_CTX_set_default_passwd_cb or PEM_read_bio_X509 last 2 args + X509_ptr x509(PEM_read_bio_X509(cert.get(), nullptr, nullptr, nullptr)); + EVP_PKEY_ptr evp_pkey(PEM_read_bio_PrivateKey(pkey.get(), nullptr, nullptr, nullptr)); + + // 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 certificate on CTX vs SSL if (SSL_CTX_use_certificate(m_ctx.get(), x509.get()) <= 0) { throw std::runtime_error(std::string("Failed to set certificate: ") + TlsSocket::tls_strerror(0)); } @@ -121,14 +148,14 @@ namespace CppSockets { } // TODO add SSL_get_shutdown checks in read operations - std::string TlsSocket::read(std::size_t len) { + auto TlsSocket::read(std::size_t len) -> std::string { std::array buff = {0}; std::stringstream res; std::size_t nb = 0; std::size_t total; if (SSL_peek(m_ssl.get(), buff.data(), BUFF_SIZE) <= 0) { - m_is_connected = false; // TODO: we should replace this with check_for_error + set_connected(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()); @@ -142,7 +169,7 @@ namespace CppSockets { return res.str(); } - std::size_t TlsSocket::read(char *buff, std::size_t size) { + 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); @@ -150,20 +177,20 @@ namespace CppSockets { return nb; } - std::size_t TlsSocket::write(const char *buff, std::size_t len) { + 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(), (void *)buff, len, &nb); + int ret = SSL_write_ex(m_ssl.get(), static_cast(buff), len, &nb); check_for_error("Failed to write to socket: ", ret); return nb; } - void TlsSocket::check_for_error(std::string error_msg, int ret) { + void TlsSocket::check_for_error(const 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 - m_is_connected = false; + set_connected(false); } if (ret <= 0) { @@ -171,7 +198,7 @@ namespace CppSockets { } } - int TlsSocket::connect(const IEndpoint &endpoint) { + auto TlsSocket::connect(const IEndpoint &endpoint) -> int { int ret = Socket::connect(endpoint); int ssl_ret = SSL_connect(m_ssl.get()); @@ -182,18 +209,32 @@ namespace CppSockets { return ret; } - std::shared_ptr TlsSocket::accept(void *addr_out) { - auto res = Socket::accept(addr_out); - std::shared_ptr tls; + auto TlsSocket::accept(const SSL_CTX_ptr &ctx) -> std::unique_ptr { + return accept(nullptr, ctx); + } + + auto TlsSocket::accept(void *addr_out, const SSL_CTX_ptr &ctx) -> std::unique_ptr { + std::unique_ptr res = Socket::accept(addr_out); + std::unique_ptr tls; int ssl_ret = 0; + SSL_CTX *raw_ctx = ctx.get(); - if (!res) + if (!res) { return nullptr; - if (SSL_CTX_up_ref(m_ctx.get()) == 0) { + } + if (!raw_ctx) { + raw_ctx = m_ctx.get(); + } + + // TODO: Not really sure we should do this. The TlsSockets shouldn't own the CTX. + // But since its already ref-counted, a shared_ptr doesnt make sense... + // Currently required cause the Constructor will get the CTX from the SSL object, and place it in a unique_ptr. + // Which means if will call CTX_free on TlsSocket destroyed. So we need to up_ref so it doesnt free for real. + if (SSL_CTX_up_ref(raw_ctx) == 0) { throw std::runtime_error("Failed to DUP SSL_CTX: " + TlsSocket::tls_strerror(0)); } - tls = std::make_shared(std::move(*res), SSL_ptr(SSL_new(m_ctx.get()), SSL_free)); + tls = std::make_unique(std::move(*res.release()), SSL_ptr(SSL_new(raw_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)); @@ -202,21 +243,7 @@ namespace CppSockets { return tls; } - static std::string extract_errors_from_queue() { - 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(); - } - - const std::string TlsSocket::tls_strerror(int ret) { + auto TlsSocket::tls_strerror(int ret) -> std::string { int err = SSL_get_error(m_ssl.get(), ret); switch (err) { @@ -241,10 +268,10 @@ namespace CppSockets { case SSL_ERROR_WANT_CLIENT_HELLO_CB: return "Retry the operation later: WANT_CLIENT_HELLO_CB"; case SSL_ERROR_SYSCALL: - m_is_connected = false; + set_connected(false); return std::string("Fatal system error: ") + Socket::strerror() + '\n' + extract_errors_from_queue(); case SSL_ERROR_SSL: - m_is_connected = false; + set_connected(false); return "Fatal TLS error: " + extract_errors_from_queue(); default: return "Unknown error: " + std::to_string(err) + '\n' + extract_errors_from_queue(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a236832..256fb27 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 Sat Dec 2 17:46:03 2023 Francois Michaut +## Last update Sat Aug 2 18:06:42 2025 Francois Michaut ## ## CMakeLists.txt : CMake building and running tests for CppSockets ## @@ -23,7 +23,7 @@ target_compile_definitions(unit_tests PRIVATE DEBUG) target_link_libraries(unit_tests cppsockets) foreach (test ${TestFiles}) - if (NOT ${test} STREQUAL test_driver.cpp) + if (NOT ${test} MATCHES "test_driver.cpp$") get_filename_component (DName ${test} DIRECTORY) get_filename_component (TName ${test} NAME_WE) if (DName STREQUAL "") diff --git a/tests/TestSockets.cpp b/tests/TestSockets.cpp index 316f966..fcd3c04 100644 --- a/tests/TestSockets.cpp +++ b/tests/TestSockets.cpp @@ -4,23 +4,31 @@ ** Author Francois Michaut ** ** Started on Mon Feb 14 21:17:55 2022 Francois Michaut -** Last update Wed Dec 6 01:34:58 2023 Francois Michaut +** Last update Tue Aug 5 11:11:27 2025 Francois Michaut ** ** TestSockets.cpp : Socket tests */ +#include "CppSockets/OSDetection.hpp" +#include "CppSockets/Socket.hpp" + +#ifndef OS_WINDOWS + +#include #include #include #include -#include "CppSockets/Socket.hpp" - -#include +#endif using namespace CppSockets; -int TestSockets(int, char **) +int TestSockets(int /* ac */, char ** const /* av */) { +#ifdef OS_WINDOWS + // TODO + return 0; +#else int child = fork(); std::string test = "Hello Network !"; int port = 44444; @@ -56,4 +64,5 @@ int TestSockets(int, char **) soc.write(test); return 0; } +#endif }