Compare commits

...

9 Commits

Author SHA1 Message Date
yhirose b1792ef29c Release v0.45.1 2026-05-24 20:58:48 -04:00
yhirose 0f3d063f0a ci: add best-effort BoringSSL job (#2456)
Adds Ubuntu and macOS CI jobs that build BoringSSL from source and exercise cpp-httplib's existing OpenSSL backend path (continue-on-error: best-effort). Makes SSLClientServerTest.TlsVerifyHostname backend-aware (BoringSSL is SAN-only per RFC 6125 §6.4.4). README notes BoringSSL as a best-effort variant with the C++14 and SAN-only caveats.
2026-05-24 02:48:46 -04:00
sakurai-ryuhei 0d7d637466 Fix zstd detection in installed httplibConfig.cmake (#2453) 2026-05-23 11:59:58 -04:00
yhirose 1ff0c8588d Fix iOS build break and modernize macOS Keychain cert loading (#2455)
* Replace deprecated SecTrustCopyAnchorCertificates on macOS

SecTrustCopyAnchorCertificates was deprecated in macOS 13. Switch to
SecTrustSettingsCopyCertificates, iterating over the System, Admin, and
User trust domains to retain equivalent coverage of anchor certificates.

* Restrict Keychain cert loading to macOS

TARGET_OS_MAC is true on all Apple platforms including iOS, tvOS, and
watchOS, which caused the keychain enumeration path to be compiled on
iOS where SecTrustSettingsCopyCertificates is unavailable.

Narrow the auto-enable and the Security.h include guards to
TARGET_OS_OSX, and emit an explicit #error when the user defines
CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN on a non-macOS Apple platform,
directing them to use set_ca_cert_path() with a bundled CA file.

Addresses the iOS build break reported in #2454.

* Add iOS header parse check to CI

Run a cross-compile syntax check against the iOS SDK to catch
accidental use of macOS-only APIs or guards (e.g. TARGET_OS_MAC vs
TARGET_OS_OSX) that would silently break iOS builds. Also verify that
defining CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN on iOS fires the
expected #error.

iOS is not officially supported as a runtime target; this job only
guarantees the header stays parse-clean on iOS toolchains.
2026-05-23 08:39:45 -04:00
NsPro04 b1cc8095a8 Specifying "Server::stop()" as noexcept (#2451)
* The current implementation of "Server::stop()" doesn't throw an exception, so why not specify this explicitly?

* Adding the missing "noexcept" to the declaration
2026-05-16 09:50:08 -04:00
yhirose 28f8264d13 Release v0.45.0 2026-05-15 09:22:11 +09:00
yhirose 91271c062d Fix keep-alive corruption on requests without framed body (#2450) 2026-05-15 06:57:51 +09:00
yhirose d755c43d58 Extract has_framed_body and is_connection_persistent helpers 2026-05-15 06:56:16 +09:00
yhirose 5c9285776e Fix crash on empty X-Forwarded-For with trusted proxies configured 2026-05-14 23:19:36 +09:00
6 changed files with 414 additions and 69 deletions
+197
View File
@@ -120,6 +120,155 @@ jobs:
- name: build and run ThreadPool test
run: cd test && make test_thread_pool && ./test_thread_pool
# BoringSSL is Google's fork of OpenSSL. It has no API stability guarantee
# and is not packaged by distros, so we build it from source. cpp-httplib
# treats it as an OpenSSL backend variant via the OPENSSL_IS_BORINGSSL
# macro (see httplib.h). This job is best-effort: continue-on-error keeps
# upstream API drift from blocking PRs while still surfacing breakage.
ubuntu-boringssl:
runs-on: ubuntu-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
continue-on-error: true
name: ubuntu (boringssl, best-effort)
env:
# Tracking HEAD keeps us honest about upstream churn. If breakage
# becomes routine, replace HEAD with a 40-char commit SHA; the
# resolve step uses the SHA directly when it matches that shape.
BORINGSSL_REF: HEAD
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
steps:
- name: checkout
uses: actions/checkout@v4
- name: install common libraries
run: |
sudo apt-get update
sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
- name: resolve BoringSSL commit
id: boringssl-rev
# Accept either a ref name (resolved via git ls-remote) or a full
# 40-char SHA used directly. ls-remote does not list arbitrary
# commit SHAs, so pinning requires the second path.
run: |
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
sha="${BORINGSSL_REF}"
echo "Using pinned BoringSSL SHA: ${sha}"
else
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
if [ -z "$sha" ]; then
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
exit 1
fi
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
fi
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
- name: cache BoringSSL build
id: boringssl-cache
uses: actions/cache@v4
with:
path: ${{ env.BORINGSSL_PREFIX }}
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
- name: build BoringSSL
if: steps.boringssl-cache.outputs.cache-hit != 'true'
run: |
set -e
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
cmake --build build -j"$(nproc)" --target install
- name: build and run tests (BoringSSL)
# Override OPENSSL_SUPPORT to point the existing OpenSSL Makefile path
# at BoringSSL's prefix. BoringSSL defines OPENSSL_IS_BORINGSSL in
# <openssl/base.h>, which httplib.h and test.cc use to switch on API
# differences (e.g. SAN-only hostname verification, no CN fallback).
#
# BoringSSL's public headers (<openssl/stack.h>) use std::enable_if_t,
# so consumers must compile with C++14 or later. cpp-httplib itself
# supports C++11, but anyone pairing it with BoringSSL inherits this
# constraint. EXTRA_CXXFLAGS appends after the Makefile's -std=c++11
# and the later flag wins.
run: |
cd test
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -lpthread"
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
env:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
# macOS counterpart of the BoringSSL job. Same best-effort posture; the
# extra framework links cover the macOS Keychain integration that
# httplib.h auto-enables for any TLS backend on macOS.
macos-boringssl:
runs-on: macos-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
continue-on-error: true
name: macos (boringssl, best-effort)
env:
BORINGSSL_REF: HEAD
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
steps:
- name: checkout
uses: actions/checkout@v4
- name: resolve BoringSSL commit
id: boringssl-rev
# Accept either a ref name (resolved via git ls-remote) or a full
# 40-char SHA used directly. ls-remote does not list arbitrary
# commit SHAs, so pinning requires the second path.
run: |
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
sha="${BORINGSSL_REF}"
echo "Using pinned BoringSSL SHA: ${sha}"
else
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
if [ -z "$sha" ]; then
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
exit 1
fi
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
fi
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
- name: cache BoringSSL build
id: boringssl-cache
uses: actions/cache@v4
with:
path: ${{ env.BORINGSSL_PREFIX }}
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
- name: build BoringSSL
if: steps.boringssl-cache.outputs.cache-hit != 'true'
run: |
set -e
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
cmake --build build -j"$(sysctl -n hw.ncpu)" --target install
- name: build and run tests (BoringSSL)
run: |
cd test
# CoreFoundation/Security frameworks satisfy the Keychain integration
# auto-enabled in httplib.h for macOS TLS builds.
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -framework CoreFoundation -framework Security"
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
env:
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
# Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
# On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous
# DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb.
@@ -250,6 +399,54 @@ jobs:
- name: build and run ThreadPool test
run: cd test && make test_thread_pool && ./test_thread_pool
ios-parse-check:
runs-on: macos-latest
if: >
(github.event_name == 'push') ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
name: ios header parse check (not officially supported)
steps:
- name: checkout
uses: actions/checkout@v4
- name: install OpenSSL headers
run: brew install openssl@3
- name: verify header parses on iOS target
run: |
IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
OPENSSL_INC=$(brew --prefix openssl@3)/include
echo "Using iOS SDK: $IOS_SDK"
echo '#include "httplib.h"' | clang++ \
-isysroot "$IOS_SDK" \
-target arm64-apple-ios16.0 \
-std=c++11 \
-DCPPHTTPLIB_OPENSSL_SUPPORT \
-I"$OPENSSL_INC" \
-I. -Wall -Wextra \
-fsyntax-only -x c++ -
- name: verify CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN is rejected on iOS
run: |
IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
OPENSSL_INC=$(brew --prefix openssl@3)/include
out=$(echo '#include "httplib.h"' | clang++ \
-isysroot "$IOS_SDK" \
-target arm64-apple-ios16.0 \
-std=c++11 \
-DCPPHTTPLIB_OPENSSL_SUPPORT \
-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN \
-I"$OPENSSL_INC" \
-I. \
-fsyntax-only -x c++ - 2>&1 || true)
if echo "$out" | grep -q "only supported on macOS"; then
echo "OK: #error fired as expected"
else
echo "FAIL: expected #error did not fire"
echo "--- compiler output ---"
echo "$out"
exit 1
fi
windows:
runs-on: windows-latest
if: >
+3
View File
@@ -73,6 +73,9 @@ cpp-httplib supports multiple TLS backends through an abstraction layer:
> [!NOTE]
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable.
> [!NOTE]
> **BoringSSL (best-effort):** BoringSSL builds under `CPPHTTPLIB_OPENSSL_SUPPORT` and is exercised by CI against current upstream. Because BoringSSL does not guarantee API stability, support is best-effort — breakage may occasionally land. Two known behavioral differences vs OpenSSL: (1) BoringSSL's public headers require C++14 or later, so consumers must compile accordingly; (2) hostname verification is SAN-only per RFC 6125 §6.4.4 (no CN fallback).
```c++
// Use either OpenSSL, Mbed TLS, or wolfSSL
#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
+1 -1
View File
@@ -61,7 +61,7 @@ if(@HTTPLIB_IS_USING_ZSTD@)
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED)
set(httplib_fd_zstd_required_arg REQUIRED)
endif()
find_package(zstd QUIET)
find_package(zstd 1.5.6 CONFIG QUIET)
if(NOT zstd_FOUND)
find_package(PkgConfig ${httplib_fd_zstd_quiet_arg} ${httplib_fd_zstd_required_arg})
if(PKG_CONFIG_FOUND)
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.44.0"
version = "0.45.1"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+93 -67
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.44.0"
#define CPPHTTPLIB_VERSION_NUM "0x002c00"
#define CPPHTTPLIB_VERSION "0.45.1"
#define CPPHTTPLIB_VERSION_NUM "0x002d01"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -339,16 +339,26 @@ using socket_t = int;
#include <utility>
// On macOS with a TLS backend, enable Keychain root certificates by default
// unless the user explicitly opts out.
// unless the user explicitly opts out. Not enabled on iOS/tvOS/watchOS since
// the SecTrustSettings APIs used to enumerate anchor certificates are macOS
// only; on those platforms the user must provide a CA bundle explicitly.
#if defined(__APPLE__) && defined(__clang__) && \
!defined(CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES) && \
(defined(CPPHTTPLIB_OPENSSL_SUPPORT) || \
defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || \
defined(CPPHTTPLIB_WOLFSSL_SUPPORT))
#if TARGET_OS_OSX
#ifndef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#define CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#endif
#endif
#endif
#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && \
defined(__APPLE__) && !TARGET_OS_OSX
#error \
"CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN is only supported on macOS. On iOS/tvOS/watchOS, supply a CA bundle via set_ca_cert_path()."
#endif
// On Windows, enable Schannel certificate verification by default
// unless the user explicitly opts out.
@@ -382,7 +392,7 @@ using socket_t = int;
#endif // _WIN32
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#if TARGET_OS_MAC
#if TARGET_OS_OSX
#include <Security/Security.h>
#endif
#endif
@@ -430,7 +440,7 @@ using socket_t = int;
#endif
#endif // _WIN32
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#if TARGET_OS_MAC
#if TARGET_OS_OSX
#include <Security/Security.h>
#endif
#endif
@@ -473,7 +483,7 @@ using socket_t = int;
#endif
#endif // _WIN32
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
#if TARGET_OS_MAC
#if TARGET_OS_OSX
#include <Security/Security.h>
#endif
#endif
@@ -1597,7 +1607,7 @@ private:
std::regex regex_;
};
int close_socket(socket_t sock);
int close_socket(socket_t sock) noexcept;
ssize_t write_headers(Stream &strm, const Headers &headers);
@@ -1734,7 +1744,7 @@ public:
bool is_running() const;
void wait_until_ready() const;
void stop();
void stop() noexcept;
void decommission();
std::function<TaskQueue *(void)> new_task_queue;
@@ -3028,8 +3038,6 @@ bool parse_range_header(const std::string &s, Ranges &ranges);
bool parse_accept_header(const std::string &s,
std::vector<std::string> &content_types);
int close_socket(socket_t sock);
ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags);
ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags);
@@ -5422,7 +5430,7 @@ inline void mmap::close() {
#endif
size_ = 0;
}
inline int close_socket(socket_t sock) {
inline int close_socket(socket_t sock) noexcept {
#ifdef _WIN32
return closesocket(sock);
#else
@@ -5649,7 +5657,7 @@ inline bool process_client_socket(
return callback(strm);
}
inline int shutdown_socket(socket_t sock) {
inline int shutdown_socket(socket_t sock) noexcept {
#ifdef _WIN32
return shutdown(sock, SD_BOTH);
#else
@@ -8578,17 +8586,24 @@ write_multipart_ranges_data(Stream &strm, const Request &req, Response &res,
});
}
inline bool has_framed_body(const Request &req) {
return is_chunked_transfer_encoding(req.headers) ||
req.get_header_value_u64("Content-Length") > 0;
}
inline bool is_connection_persistent(const Request &req) {
auto conn = req.get_header_value("Connection");
if (conn == "close") { return false; }
if (req.version == "HTTP/1.0" && conn != "Keep-Alive") { return false; }
return true;
}
inline bool expect_content(const Request &req) {
if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
req.method == "DELETE") {
return true;
}
if (req.has_header("Content-Length") &&
req.get_header_value_u64("Content-Length") > 0) {
return true;
}
if (is_chunked_transfer_encoding(req.headers)) { return true; }
return false;
return has_framed_body(req);
}
#ifdef _WIN32
@@ -10997,7 +11012,7 @@ inline void Server::wait_until_ready() const {
}
}
inline void Server::stop() {
inline void Server::stop() noexcept {
if (is_running_) {
assert(svr_sock_ != INVALID_SOCKET);
std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
@@ -11304,29 +11319,18 @@ inline bool Server::read_content_core(
size_t /*len*/) { return receiver(buf, n); };
}
// RFC 7230 Section 3.3.3: If this is a request message and none of the above
// are true (no Transfer-Encoding and no Content-Length), then the message
// body length is zero (no message body is present).
//
// For non-SSL builds, detect clients that send a body without a
// Content-Length header (raw HTTP over TCP). Check both the stream's
// internal read buffer (data already read from the socket during header
// parsing) and the socket itself for pending data. If data is found and
// exceeds the configured payload limit, reject with 413.
// For SSL builds we cannot reliably peek the decrypted application bytes,
// so keep the original behaviour.
// RFC 9112 §6: no Transfer-Encoding and no Content-Length means no body.
// For non-SSL builds we still scan non-persistent connections for stray
// body bytes so the payload limit is enforced (413). On keep-alive,
// pending bytes may be the next request (issue #2450), so skip.
#if !defined(CPPHTTPLIB_SSL_ENABLED)
if (!req.has_header("Content-Length") &&
!detail::is_chunked_transfer_encoding(req.headers)) {
// Only check if payload_max_length is set to a finite value
if (payload_max_length_ > 0 &&
if (!detail::is_connection_persistent(req) && payload_max_length_ > 0 &&
payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
// Check if there is data already buffered in the stream (read during
// header parsing) or pending on the socket. Use a non-blocking socket
// check to avoid deadlock when the client sends no body.
bool has_data = strm.is_readable();
auto has_data = strm.is_readable();
if (!has_data) {
socket_t s = strm.socket();
auto s = strm.socket();
if (s != INVALID_SOCKET) {
has_data = detail::select_read(s, 0, 0) > 0;
}
@@ -11888,6 +11892,11 @@ get_client_ip(const std::string &x_forwarded_for,
ip_list.emplace_back(std::string(b + r.first, b + r.second));
});
// A malformed X-Forwarded-For (empty, comma-only, whitespace-only) yields
// no segments. Signal "no client IP derived" with an empty string so the
// caller can fall back to the connection-level remote address.
if (ip_list.empty()) { return std::string(); }
for (size_t i = 0; i < ip_list.size(); ++i) {
auto ip = ip_list[i];
@@ -11978,7 +11987,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) {
auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_);
auto derived = get_client_ip(x_forwarded_for, trusted_proxies_);
req.remote_addr = derived.empty() ? remote_addr : derived;
} else {
req.remote_addr = remote_addr;
}
@@ -12180,15 +12190,14 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
ret = write_response(strm, close_connection, req, res);
}
// Drain any unconsumed request body to prevent request smuggling on
// keep-alive connections.
if (!req.body_consumed_ && detail::expect_content(req)) {
int drain_status = 200; // required by read_content signature
// Drain any unconsumed framed body to prevent request smuggling on
// keep-alive. Without framing there is no body to drain — reading would
// consume the next request (issue #2450).
if (!req.body_consumed_ && detail::has_framed_body(req)) {
int dummy_status;
if (!detail::read_content(
strm, req, payload_max_length_, drain_status, nullptr,
strm, req, payload_max_length_, dummy_status, nullptr,
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
// Body exceeds payload limit or read error — close the connection
// to prevent leftover bytes from being misinterpreted.
connection_closed = true;
}
}
@@ -16144,9 +16153,18 @@ inline bool enumerate_windows_system_certs(Callback cb) {
template <typename Callback>
inline bool enumerate_macos_keychain_certs(Callback cb) {
bool loaded = false;
CFArrayRef certs = nullptr;
OSStatus status = SecTrustCopyAnchorCertificates(&certs);
if (status == errSecSuccess && certs) {
const SecTrustSettingsDomain domains[] = {
kSecTrustSettingsDomainSystem,
kSecTrustSettingsDomainAdmin,
kSecTrustSettingsDomainUser,
};
for (auto domain : domains) {
CFArrayRef certs = nullptr;
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
if (status != errSecSuccess || !certs) {
if (certs) CFRelease(certs);
continue;
}
CFIndex count = CFArrayGetCount(certs);
for (CFIndex i = 0; i < count; i++) {
SecCertificateRef cert =
@@ -16509,28 +16527,36 @@ inline bool load_system_certs(ctx_t ctx) {
auto store = SSL_CTX_get_cert_store(ssl_ctx);
if (!store) return false;
CFArrayRef certs = nullptr;
if (SecTrustCopyAnchorCertificates(&certs) != errSecSuccess || !certs) {
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
}
bool loaded_any = false;
auto count = CFArrayGetCount(certs);
for (CFIndex i = 0; i < count; i++) {
auto cert = reinterpret_cast<SecCertificateRef>(
const_cast<void *>(CFArrayGetValueAtIndex(certs, i)));
CFDataRef der = SecCertificateCopyData(cert);
if (der) {
const unsigned char *data = CFDataGetBytePtr(der);
auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der));
if (x509) {
if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
X509_free(x509);
}
CFRelease(der);
const SecTrustSettingsDomain domains[] = {
kSecTrustSettingsDomainSystem,
kSecTrustSettingsDomainAdmin,
kSecTrustSettingsDomainUser,
};
for (auto domain : domains) {
CFArrayRef certs = nullptr;
if (SecTrustSettingsCopyCertificates(domain, &certs) != errSecSuccess ||
!certs) {
if (certs) CFRelease(certs);
continue;
}
auto count = CFArrayGetCount(certs);
for (CFIndex i = 0; i < count; i++) {
auto cert = reinterpret_cast<SecCertificateRef>(
const_cast<void *>(CFArrayGetValueAtIndex(certs, i)));
CFDataRef der = SecCertificateCopyData(cert);
if (der) {
const unsigned char *data = CFDataGetBytePtr(der);
auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der));
if (x509) {
if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
X509_free(x509);
}
CFRelease(der);
}
}
CFRelease(certs);
}
CFRelease(certs);
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
#else
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
+119
View File
@@ -10673,8 +10673,20 @@ TEST(SSLClientServerTest, TlsVerifyHostname) {
<< "Verify callback should have been called";
// CN="Common Name" should match our test certificate
//
// BoringSSL intentionally drops CN-based hostname matching per RFC 6125
// §6.4.4 — only SubjectAltName is consulted. Other backends (OpenSSL,
// MbedTLS, wolfSSL) still honor the CN fallback, so flip the expectation
// for BoringSSL builds. OPENSSL_IS_BORINGSSL is defined by BoringSSL's
// <openssl/base.h>, which is included transitively when
// CPPHTTPLIB_OPENSSL_SUPPORT is set against a BoringSSL install.
#if defined(OPENSSL_IS_BORINGSSL)
EXPECT_FALSE(verify_result_cn)
<< "BoringSSL should reject CN-based hostname matching (SAN-only)";
#else
EXPECT_TRUE(verify_result_cn)
<< "verify_hostname should match 'Common Name' (certificate CN)";
#endif
// Wrong hostname should not match
EXPECT_FALSE(verify_result_wrong)
@@ -14251,6 +14263,53 @@ TEST(ForwardedHeadersTest, HandlesWhitespaceAroundIPs) {
EXPECT_EQ(observed_remote_addr, "203.0.113.66");
}
// An X-Forwarded-For header whose value parses to zero IP segments must not
// crash the server (it used to call front() on an empty vector inside
// get_client_ip). The connection-level remote address must be retained instead.
static void run_malformed_xff_test(const std::string &xff_value) {
Server svr;
svr.set_trusted_proxies({"192.0.2.45"});
std::string observed_remote_addr;
svr.Get("/ip", [&](const Request &req, Response &res) {
observed_remote_addr = req.remote_addr;
res.set_content("ok", "text/plain");
});
int port = 0;
thread t = thread([&]() {
port = svr.bind_to_any_port(HOST);
svr.listen_after_bind();
});
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
auto res = cli.Get("/ip", {{"X-Forwarded-For", xff_value}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_TRUE(observed_remote_addr == "::1" ||
observed_remote_addr == "127.0.0.1");
}
TEST(ForwardedHeadersTest, EmptyXForwardedFor_DoesNotCrash) {
run_malformed_xff_test("");
}
TEST(ForwardedHeadersTest, CommaOnlyXForwardedFor_DoesNotCrash) {
run_malformed_xff_test(",");
}
TEST(ForwardedHeadersTest, MultipleCommasXForwardedFor_DoesNotCrash) {
run_malformed_xff_test(", , ,");
}
#ifndef _WIN32
TEST(ServerRequestParsingTest, RequestWithoutContentLengthOrTransferEncoding) {
Server svr;
@@ -18157,3 +18216,63 @@ TEST(RequestSmugglingTest, ContentLengthAndTransferEncodingRejected) {
response.substr(0, response.find("\r\n")));
}
}
// Regression for issue #2450: a DELETE without Content-Length on a
// keep-alive connection must not let the post-response drain consume the
// next request's bytes.
TEST(KeepAliveTest, DeleteWithoutContentLengthDoesNotEatNextRequest) {
Server svr;
std::atomic<int> delete_count(0);
svr.Delete("/items/:id", [&](const Request &, Response &res) {
delete_count++;
res.status = StatusCode::NoContent_204;
});
auto port = svr.bind_to_any_port(HOST);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
});
svr.wait_until_ready();
auto error = Error::Success;
auto sock = detail::create_client_socket(
HOST, "", port, AF_UNSPEC, false, false, nullptr,
/*connection_timeout_sec=*/2, 0,
/*read_timeout_sec=*/2, 0,
/*write_timeout_sec=*/2, 0, std::string(), error);
ASSERT_NE(INVALID_SOCKET, sock);
auto sock_se = detail::scope_exit([&] { detail::close_socket(sock); });
auto send_request_and_read_response = [&](const std::string &req,
std::string &out) -> bool {
auto sent = send(sock, req.data(), req.size(), 0);
if (sent != static_cast<ssize_t>(req.size())) { return false; }
char buf[4096];
for (;;) {
auto n = recv(sock, buf, sizeof(buf), 0);
if (n <= 0) { return !out.empty(); }
out.append(buf, static_cast<size_t>(n));
if (out.find("\r\n\r\n") != std::string::npos) { return true; }
}
};
std::string req1 = "DELETE /items/1 HTTP/1.1\r\n"
"Host: localhost\r\n"
"\r\n";
std::string resp1;
ASSERT_TRUE(send_request_and_read_response(req1, resp1));
EXPECT_NE(std::string::npos, resp1.find("HTTP/1.1 204"));
std::string req2 = "DELETE /items/2 HTTP/1.1\r\n"
"Host: localhost\r\n"
"Connection: close\r\n"
"\r\n";
std::string resp2;
ASSERT_TRUE(send_request_and_read_response(req2, resp2));
EXPECT_NE(std::string::npos, resp2.find("HTTP/1.1 204"));
EXPECT_EQ(2, delete_count.load());
}