Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions ci/check-unit-suites.sh
Original file line number Diff line number Diff line change
@@ -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 <LastTest.log>}"

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
3 changes: 3 additions & 0 deletions kmipclient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
147 changes: 119 additions & 28 deletions kmipclient/src/NetClientOpenSSL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,25 @@

#include "kmipclient/KmipIOException.hpp"

#include <arpa/inet.h>
#include <cerrno>
#include <chrono>
#include <cstring>
#include <fcntl.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509_vfy.h>
#include <poll.h>
#include <openssl/x509v3.h>
#include <sstream>
#include <sys/socket.h>
#include <sys/time.h>

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/time.h>
#endif

namespace kmipclient {

Expand All @@ -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<DWORD>(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.
Expand Down Expand Up @@ -205,8 +260,13 @@ namespace kmipclient {
remaining_ms = 1;
}

#ifdef _WIN32
WSAPOLLFD pfd{};
pfd.fd = static_cast<SOCKET>(fd);
#else
struct pollfd pfd{};
pfd.fd = fd;
#endif
if (BIO_should_read(bio)) {
pfd.events |= POLLIN;
}
Expand All @@ -218,9 +278,13 @@ namespace kmipclient {
}

int poll_ret = 0;
#ifdef _WIN32
poll_ret = WSAPoll(&pfd, 1, static_cast<int>(remaining_ms));
#else
do {
poll_ret = poll(&pfd, 1, static_cast<int>(remaining_ms));
} while (poll_ret < 0 && errno == EINTR);
#endif

if (poll_ret == 0) {
throw KmipIOException(
Expand All @@ -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())
);
}
}
Expand All @@ -245,21 +309,34 @@ namespace kmipclient {
);
}

#ifdef _WIN32
u_long blocking_mode = 0;
if (ioctlsocket(static_cast<SOCKET>(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())
);
}

const int desired_flags = flags & ~O_NONBLOCK;
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
Expand All @@ -276,32 +353,44 @@ namespace kmipclient {
return;
}

#ifdef _WIN32
const DWORD tv = static_cast<DWORD>(timeout_ms);
const SOCKET sock = static_cast<SOCKET>(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<const char *>(&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<const char *>(&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;
Expand Down Expand Up @@ -492,9 +581,10 @@ namespace kmipclient {
return -1;
}
const int dlen = static_cast<int>(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)
);
Expand All @@ -507,9 +597,10 @@ namespace kmipclient {
return -1;
}
const int dlen = static_cast<int>(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)
);
Expand Down
4 changes: 3 additions & 1 deletion kmipcore/include/kmipcore/kmip_formatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
20 changes: 14 additions & 6 deletions kmipcore/src/kmip_basics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,29 @@
#include "kmipcore/kmip_errors.hpp"
#include "kmipcore/serialization_buffer.hpp"

#include <arpa/inet.h>
#include <bit>
#include <cstring>
#include <iomanip>
#include <vector>

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<std::uint64_t>(low) << 32) | high;
if constexpr (std::endian::native == std::endian::big) {
return v;
}
return (static_cast<std::uint64_t>(to_be32(static_cast<std::uint32_t>(v)))
<< 32) |
to_be32(static_cast<std::uint32_t>(v >> 32));
}
// Safe big-endian decoders from raw byte spans.
static std::uint32_t
Expand Down
Loading