diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cf6847..77e9c92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,3 +119,64 @@ jobs: run: | cmake -S . -B build-docs -DBUILD_DOCS=ON cmake --build build-docs --target doc + + macos: + runs-on: macos-15 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: brew install ninja openssl@3 + + - name: Configure + run: > + cmake -S . -B build -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DBUILD_TESTS=ON + -DOPENSSL_ROOT_DIR=$(brew --prefix openssl@3) + + - name: Build + run: cmake --build build -j + + - name: Test + run: ctest --test-dir build --output-on-failure + + - name: Check unit suites ran + run: ci/check-unit-suites.sh build/Testing/Temporary/LastTest.log + + windows: + runs-on: windows-2025 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - name: Install OpenSSL + run: choco install openssl -y --no-progress + + - name: Locate OpenSSL + shell: bash + run: | + for dir in "/c/Program Files/OpenSSL" "/c/Program Files/OpenSSL-Win64"; do + if [ -e "$dir/include/openssl/ssl.h" ]; then + win=$(cygpath -w "$dir") + echo "OPENSSL_ROOT_DIR=$win" >> "$GITHUB_ENV" + echo "$win\\bin" >> "$GITHUB_PATH" + exit 0 + fi + done + echo "::error::OpenSSL install not found" + exit 1 + + - name: Configure + run: cmake -S . -B build -DBUILD_TESTS=ON + + - name: Build + run: cmake --build build --config Release --parallel + + - name: Test + run: ctest --test-dir build -C Release --output-on-failure + + - name: Check unit suites ran + shell: bash + run: ci/check-unit-suites.sh build/Testing/Temporary/LastTest.log diff --git a/ci/check-unit-suites.sh b/ci/check-unit-suites.sh new file mode 100755 index 0000000..e905960 --- /dev/null +++ b/ci/check-unit-suites.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Fails when any of the platform-independent unit suites is absent from the +# ctest log, so an accidentally-empty test run cannot pass CI. +set -euo pipefail + +log="${1:?usage: check-unit-suites.sh }" + +status=0 +for suite in kmip_core_test kmip_parser_test kmip_serialization_buffer_test IOUtilsTest; do + if ! grep -q "$suite" "$log"; then + echo "::error::unit suite $suite did not run" + status=1 + fi +done +exit $status diff --git a/kmipclient/CMakeLists.txt b/kmipclient/CMakeLists.txt index 7ecf46f..1b032d8 100644 --- a/kmipclient/CMakeLists.txt +++ b/kmipclient/CMakeLists.txt @@ -45,6 +45,9 @@ target_include_directories( ) target_link_libraries(kmipclient PUBLIC kmipcore OpenSSL::SSL OpenSSL::Crypto Threads::Threads) +if(WIN32) + target_link_libraries(kmipclient PUBLIC ws2_32) +endif() set_property(TARGET kmipclient PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/kmipclient/src/NetClientOpenSSL.cpp b/kmipclient/src/NetClientOpenSSL.cpp index c269efb..db9d7c6 100644 --- a/kmipclient/src/NetClientOpenSSL.cpp +++ b/kmipclient/src/NetClientOpenSSL.cpp @@ -9,19 +9,25 @@ #include "kmipclient/KmipIOException.hpp" -#include #include #include #include -#include #include #include #include #include -#include +#include #include -#include -#include + +#ifdef _WIN32 + #include + #include +#else + #include + #include + #include + #include +#endif namespace kmipclient { @@ -41,20 +47,69 @@ namespace kmipclient { return errStr; } +#ifdef _WIN32 + static int last_socket_error() { + return WSAGetLastError(); + } + static void clear_socket_error() { + WSASetLastError(0); + } + static std::string socket_error_string(int err) { + char buf[256] = {}; + const DWORD len = FormatMessageA( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + static_cast(err), + 0, + buf, + sizeof(buf), + nullptr + ); + std::string msg(buf, len); + while (!msg.empty() && + (msg.back() == '\r' || msg.back() == '\n' || msg.back() == ' ')) { + msg.pop_back(); + } + if (msg.empty()) { + return "Winsock error " + std::to_string(err); + } + return msg; + } + // WSAETIMEDOUT: SO_RCVTIMEO/SO_SNDTIMEO expired. WSAEWOULDBLOCK is the + // Windows analogue of EAGAIN on a socket with a pending timeout. + static bool is_timeout_socket_error(int err) { + return err == WSAETIMEDOUT || err == WSAEWOULDBLOCK; + } +#else + static int last_socket_error() { + return errno; + } + static void clear_socket_error() { + errno = 0; + } + static std::string socket_error_string(int err) { + return strerror(err); + } + static bool is_timeout_socket_error(int err) { + return err == EAGAIN || err == EWOULDBLOCK || err == ETIMEDOUT; + } +#endif + static std::string timeoutMessage(const char *op, int timeout_ms) { std::ostringstream oss; oss << "KMIP " << op << " timed out after " << timeout_ms << "ms"; return oss.str(); } + // a2i_IPADDRESS is a pure parser (accepts IPv4 dotted-quad and IPv6), + // so this works before Winsock is initialized on Windows. static bool is_ip_address(const std::string &host) { - in_addr addr4{}; - if (inet_pton(AF_INET, host.c_str(), &addr4) == 1) { - return true; + ASN1_OCTET_STRING *addr = a2i_IPADDRESS(host.c_str()); + if (addr == nullptr) { + return false; } - - in6_addr addr6{}; - return inet_pton(AF_INET6, host.c_str(), &addr6) == 1; + ASN1_OCTET_STRING_free(addr); + return true; } // TLS_method() was introduced in OpenSSL 1.1.0. @@ -205,8 +260,13 @@ namespace kmipclient { remaining_ms = 1; } +#ifdef _WIN32 + WSAPOLLFD pfd{}; + pfd.fd = static_cast(fd); +#else struct pollfd pfd{}; pfd.fd = fd; +#endif if (BIO_should_read(bio)) { pfd.events |= POLLIN; } @@ -218,9 +278,13 @@ namespace kmipclient { } int poll_ret = 0; +#ifdef _WIN32 + poll_ret = WSAPoll(&pfd, 1, static_cast(remaining_ms)); +#else do { poll_ret = poll(&pfd, 1, static_cast(remaining_ms)); } while (poll_ret < 0 && errno == EINTR); +#endif if (poll_ret == 0) { throw KmipIOException( @@ -231,7 +295,7 @@ namespace kmipclient { throw KmipIOException( kmipcore::KMIP_IO_FAILURE, std::string("poll failed while waiting for ") + op + ": " + - strerror(errno) + socket_error_string(last_socket_error()) ); } } @@ -245,11 +309,22 @@ namespace kmipclient { ); } +#ifdef _WIN32 + u_long blocking_mode = 0; + if (ioctlsocket(static_cast(fd), FIONBIO, &blocking_mode) != 0) { + throw KmipIOException( + kmipcore::KMIP_IO_FAILURE, + "ioctlsocket(FIONBIO) failed: " + + socket_error_string(last_socket_error()) + ); + } +#else const int flags = fcntl(fd, F_GETFL, 0); if (flags < 0) { throw KmipIOException( kmipcore::KMIP_IO_FAILURE, - std::string("fcntl(F_GETFL) failed: ") + strerror(errno) + std::string("fcntl(F_GETFL) failed: ") + + socket_error_string(last_socket_error()) ); } @@ -257,9 +332,11 @@ namespace kmipclient { if (fcntl(fd, F_SETFL, desired_flags) != 0) { throw KmipIOException( kmipcore::KMIP_IO_FAILURE, - std::string("fcntl(F_SETFL) failed: ") + strerror(errno) + std::string("fcntl(F_SETFL) failed: ") + + socket_error_string(last_socket_error()) ); } +#endif } // Apply SO_RCVTIMEO / SO_SNDTIMEO on the underlying socket so that every @@ -276,32 +353,44 @@ namespace kmipclient { return; } +#ifdef _WIN32 + const DWORD tv = static_cast(timeout_ms); + const SOCKET sock = static_cast(fd); +#else struct timeval tv{}; tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; + const int sock = fd; +#endif - if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) { + if (setsockopt( + sock, + SOL_SOCKET, + SO_RCVTIMEO, + reinterpret_cast(&tv), + sizeof(tv) + ) != 0) { throw KmipIOException( kmipcore::KMIP_IO_FAILURE, "Failed to set SO_RCVTIMEO (" + std::to_string(timeout_ms) + - "ms): " + strerror(errno) + "ms): " + socket_error_string(last_socket_error()) ); } - if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) != 0) { + if (setsockopt( + sock, + SOL_SOCKET, + SO_SNDTIMEO, + reinterpret_cast(&tv), + sizeof(tv) + ) != 0) { throw KmipIOException( kmipcore::KMIP_IO_FAILURE, "Failed to set SO_SNDTIMEO (" + std::to_string(timeout_ms) + - "ms): " + strerror(errno) + "ms): " + socket_error_string(last_socket_error()) ); } } - // Returns true when errno indicates that a socket operation was interrupted - // by the kernel because the configured SO_RCVTIMEO / SO_SNDTIMEO expired. - static bool is_timeout_errno() { - return errno == EAGAIN || errno == EWOULDBLOCK || errno == ETIMEDOUT; - } - bool NetClientOpenSSL::checkConnected() { if (is_connected()) { return true; @@ -492,9 +581,10 @@ namespace kmipclient { return -1; } const int dlen = static_cast(data.size()); - errno = 0; + clear_socket_error(); const int ret = BIO_write(bio_.get(), data.data(), dlen); - if (ret <= 0 && BIO_should_retry(bio_.get()) && is_timeout_errno()) { + if (ret <= 0 && BIO_should_retry(bio_.get()) && + is_timeout_socket_error(last_socket_error())) { throw KmipIOException( kmipcore::KMIP_IO_FAILURE, timeoutMessage("send", m_timeout_ms) ); @@ -507,9 +597,10 @@ namespace kmipclient { return -1; } const int dlen = static_cast(data.size()); - errno = 0; + clear_socket_error(); const int ret = BIO_read(bio_.get(), data.data(), dlen); - if (ret <= 0 && BIO_should_retry(bio_.get()) && is_timeout_errno()) { + if (ret <= 0 && BIO_should_retry(bio_.get()) && + is_timeout_socket_error(last_socket_error())) { throw KmipIOException( kmipcore::KMIP_IO_FAILURE, timeoutMessage("receive", m_timeout_ms) ); diff --git a/kmipcore/include/kmipcore/kmip_formatter.hpp b/kmipcore/include/kmipcore/kmip_formatter.hpp index 6369dbf..acfae82 100644 --- a/kmipcore/include/kmipcore/kmip_formatter.hpp +++ b/kmipcore/include/kmipcore/kmip_formatter.hpp @@ -15,7 +15,9 @@ namespace kmipcore { - class Element; + // Element is defined as a struct; the forward declaration must use the + // same class-key because MSVC encodes it in mangled names (C4099). + struct Element; class RequestMessage; class ResponseMessage; diff --git a/kmipcore/src/kmip_basics.cpp b/kmipcore/src/kmip_basics.cpp index d7c4343..1e8bfdd 100644 --- a/kmipcore/src/kmip_basics.cpp +++ b/kmipcore/src/kmip_basics.cpp @@ -10,21 +10,29 @@ #include "kmipcore/kmip_errors.hpp" #include "kmipcore/serialization_buffer.hpp" -#include +#include #include #include #include namespace kmipcore { - // Helper functions for big-endian + // Helper functions for big-endian: return a value whose in-memory byte + // order is big-endian, so it can be written to the wire with memcpy. static std::uint32_t to_be32(std::uint32_t v) { - return htonl(v); + if constexpr (std::endian::native == std::endian::big) { + return v; + } + return ((v & 0x000000FFu) << 24) | ((v & 0x0000FF00u) << 8) | + ((v & 0x00FF0000u) >> 8) | ((v & 0xFF000000u) >> 24); } static std::uint64_t to_be64(std::uint64_t v) { - std::uint32_t high = htonl(v >> 32); - std::uint32_t low = htonl(v & 0xFFFFFFFF); - return (static_cast(low) << 32) | high; + if constexpr (std::endian::native == std::endian::big) { + return v; + } + return (static_cast(to_be32(static_cast(v))) + << 32) | + to_be32(static_cast(v >> 32)); } // Safe big-endian decoders from raw byte spans. static std::uint32_t