mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1792ef29c | |||
| 0f3d063f0a | |||
| 0d7d637466 | |||
| 1ff0c8588d | |||
| b1cc8095a8 | |||
| 28f8264d13 | |||
| 91271c062d | |||
| d755c43d58 | |||
| 5c9285776e | |||
| 811dd0b6f2 | |||
| e8e652824b | |||
| fbb031ed85 | |||
| 7d5082cc0e | |||
| 600d220c84 | |||
| 87d62db46b | |||
| a1fdc07f34 | |||
| eb49a304b6 | |||
| a9bfe5914b | |||
| ec5ce17929 | |||
| f6524c0802 | |||
| 35c4026c7f | |||
| 40e18460bc | |||
| 92aecf85d8 | |||
| b223e29778 | |||
| 2d2efe46da | |||
| cae753425e | |||
| d412e98c62 | |||
| 806fcb8268 | |||
| c2678f0186 | |||
| 0cbeafe6a4 | |||
| 13e866bdb0 | |||
| db6c9ef27b | |||
| 887837c65b | |||
| 3d56762d5c | |||
| 109e331068 | |||
| 2ea632264d | |||
| 511cc02278 | |||
| f50bd311fb | |||
| b0866cff8f | |||
| 5ebbfeef0b | |||
| d14e4fc05f |
@@ -21,7 +21,8 @@ jobs:
|
||||
- name: Build (Win32)
|
||||
shell: cmd
|
||||
run: |
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x86
|
||||
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set VSDIR=%%i
|
||||
call "%VSDIR%\VC\Auxiliary\Build\vcvarsall.bat" x86 || exit /b 1
|
||||
cl /std:c++14 /EHsc /W4 /WX /c /Fo:NUL test\test_32bit_build.cpp
|
||||
|
||||
test-arm32:
|
||||
|
||||
+303
-6
@@ -25,7 +25,9 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '*' }}
|
||||
# Exclude *_Online tests by default — they hit external services and flake on
|
||||
# CI runners. Run with workflow_dispatch + a custom filter to include them.
|
||||
GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '-*_Online' }}
|
||||
|
||||
jobs:
|
||||
style-check:
|
||||
@@ -75,6 +77,7 @@ jobs:
|
||||
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')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tls_backend: [openssl, mbedtls, wolfssl]
|
||||
name: ubuntu (${{ matrix.tls_backend }})
|
||||
@@ -101,7 +104,10 @@ jobs:
|
||||
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
|
||||
- name: build and run tests (Mbed TLS)
|
||||
if: matrix.tls_backend == 'mbedtls'
|
||||
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
|
||||
# Run mbedTLS shards with reduced parallelism — under ASAN+mbedTLS the
|
||||
# default 4 shards overload CI runners enough that timing-sensitive
|
||||
# ServerTest cases flake on first-request keep-alive reuse.
|
||||
run: cd test && make test_split_mbedtls && SHARDS=2 make test_mbedtls_parallel
|
||||
- name: build and run tests (wolfSSL)
|
||||
if: matrix.tls_backend == 'wolfssl'
|
||||
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
|
||||
@@ -114,6 +120,241 @@ 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.
|
||||
# When gai_suspend() hits the connection timeout, gai_cancel() is called
|
||||
# but does not block; the resolver worker can later write back into the
|
||||
# destroyed stack frame. To make the worker actually reach that write,
|
||||
# the test job runs a loopback UDP responder (test/dns_test_fixture.py)
|
||||
# that delays its reply past the test's 1s timeout, and uses an iptables
|
||||
# NAT rule so glibc's lookups land on that fixture instead of a real
|
||||
# nameserver. With ASAN's detect_stack_use_after_return enabled, the
|
||||
# late write-back is reported as a stack-use-after-return.
|
||||
issue-2431-repro:
|
||||
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')
|
||||
name: issue-2431 repro (Linux + ASAN)
|
||||
# Bound the whole job in case anything in the test harness hangs
|
||||
# unexpectedly. With the fixture in place a normal run is well under
|
||||
# a minute either way (ASAN abort on broken HEAD, clean pass on fix).
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
DNS_FIXTURE_PORT: "15353"
|
||||
DNS_FIXTURE_DELAY: "3"
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: install libraries
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libssl-dev zlib1g-dev libbrotli-dev \
|
||||
libzstd-dev libcurl4-openssl-dev iptables util-linux iproute2
|
||||
- name: start loopback DNS test fixture
|
||||
run: |
|
||||
# Force glibc through its DNS code path: Ubuntu's default
|
||||
# nsswitch short-circuits to NOTFOUND through mdns4_minimal,
|
||||
# which would skip the buggy code entirely.
|
||||
sudo sed -i 's/^hosts:.*/hosts: dns/' /etc/nsswitch.conf
|
||||
# Run the loopback fixture (delayed UDP responder).
|
||||
python3 test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \
|
||||
>/tmp/dns_fixture.log 2>&1 &
|
||||
echo $! | sudo tee /tmp/dns_fixture.pid >/dev/null
|
||||
# Wait for the fixture to start listening.
|
||||
for _ in $(seq 1 50); do
|
||||
if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" \
|
||||
|| { echo "fixture failed to start"; cat /tmp/dns_fixture.log; exit 1; }
|
||||
# Send the test process's DNS lookups to the loopback fixture.
|
||||
# NAT only the local OUTPUT chain; conntrack handles the reply path.
|
||||
sudo iptables -t nat -I OUTPUT -p udp --dport 53 \
|
||||
-j REDIRECT --to-port "$DNS_FIXTURE_PORT"
|
||||
# Sanity check: a query must take at least the fixture delay
|
||||
# and resolve to NXDOMAIN (proving traffic reaches the fixture).
|
||||
start=$(date +%s)
|
||||
getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
if [ "$elapsed" -lt 2 ]; then
|
||||
echo "ERROR: lookup returned in ${elapsed}s; fixture not in path" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)"
|
||||
- name: build test binary
|
||||
run: cd test && make test
|
||||
- name: run GetAddrInfoAsyncCancelTest
|
||||
run: |
|
||||
cd test
|
||||
ARCH=$(uname -m)
|
||||
CPPHTTPLIB_TEST_ISSUE_2431=1 \
|
||||
ASAN_OPTIONS=detect_stack_use_after_return=1 \
|
||||
LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
|
||||
setarch "$ARCH" -R \
|
||||
./test --gtest_filter='GetAddrInfoAsyncCancelTest.*'
|
||||
- name: tear down test fixture
|
||||
if: always()
|
||||
run: |
|
||||
sudo iptables -t nat -F OUTPUT || true
|
||||
if [ -f /tmp/dns_fixture.pid ]; then
|
||||
sudo kill "$(cat /tmp/dns_fixture.pid)" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
if: >
|
||||
@@ -122,6 +363,7 @@ jobs:
|
||||
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')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tls_backend: [openssl, mbedtls, wolfssl]
|
||||
name: macos (${{ matrix.tls_backend }})
|
||||
@@ -141,7 +383,10 @@ jobs:
|
||||
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
|
||||
- name: build and run tests (Mbed TLS)
|
||||
if: matrix.tls_backend == 'mbedtls'
|
||||
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
|
||||
# macOS runners under ASAN+mbedTLS still flake at SHARDS=2 (rapid
|
||||
# bind/connect on the fixture's fixed port races on the slower
|
||||
# macos-latest runner). Serialize fully here; ubuntu stays at 2.
|
||||
run: cd test && make test_split_mbedtls && SHARDS=1 make test_mbedtls_parallel
|
||||
- name: build and run tests (wolfSSL)
|
||||
if: matrix.tls_backend == 'wolfssl'
|
||||
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
|
||||
@@ -154,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: >
|
||||
@@ -162,6 +455,7 @@ jobs:
|
||||
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_windows == 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- with_ssl: false
|
||||
@@ -228,7 +522,7 @@ jobs:
|
||||
for ($i = 0; $i -lt $shards; $i++) {
|
||||
$log = "shard_${i}.log"
|
||||
$procs += Start-Process -FilePath ./Release/httplib-test.exe `
|
||||
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}" `
|
||||
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '-*_Online' }}" `
|
||||
-NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
|
||||
-Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
|
||||
}
|
||||
@@ -236,11 +530,14 @@ jobs:
|
||||
$failed = $false
|
||||
for ($i = 0; $i -lt $shards; $i++) {
|
||||
$log = "shard_${i}.log"
|
||||
if (Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet) {
|
||||
$proc = $procs[$i]
|
||||
$hasPassed = Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet
|
||||
$hasFailed = Select-String -Path $log -Pattern "\[ FAILED \]" -Quiet
|
||||
if ($hasPassed -and -not $hasFailed -and $proc.ExitCode -eq 0) {
|
||||
$passed = (Select-String -Path $log -Pattern "\[ PASSED \]").Line
|
||||
Write-Host "Shard ${i}: $passed"
|
||||
} else {
|
||||
Write-Host "=== Shard $i FAILED ==="
|
||||
Write-Host "=== Shard $i FAILED (exit=$($proc.ExitCode)) ==="
|
||||
Get-Content $log
|
||||
if (Test-Path "${log}.err") { Get-Content "${log}.err" }
|
||||
$failed = $true
|
||||
|
||||
@@ -43,6 +43,9 @@ test/test_mbedtls
|
||||
test/test_wolfssl
|
||||
test/test_no_tls
|
||||
test/server_fuzzer
|
||||
test/client_fuzzer
|
||||
test/header_parser_fuzzer
|
||||
test/url_parser_fuzzer
|
||||
test/test_proxy
|
||||
test/test_proxy_mbedtls
|
||||
test/test_proxy_wolfssl
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.43.1"
|
||||
version = "0.45.1"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.43.1"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002b01"
|
||||
#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);
|
||||
@@ -5016,12 +5024,11 @@ inline bool parse_header(const char *beg, const char *end, T fn) {
|
||||
|
||||
if (!detail::fields::is_field_value(val)) { return false; }
|
||||
|
||||
if (case_ignore::equal(key, "Location") ||
|
||||
case_ignore::equal(key, "Referer")) {
|
||||
fn(key, val);
|
||||
} else {
|
||||
fn(key, decode_path_component(val));
|
||||
}
|
||||
// RFC 9110 §5.5: header field values are opaque octets and MUST NOT be
|
||||
// percent-decoded by the recipient. Applications that need to interpret a
|
||||
// value as a URI component should call httplib::decode_uri_component()
|
||||
// (or decode_path_component()) explicitly.
|
||||
fn(key, val);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -5319,8 +5326,9 @@ inline bool mmap::open(const char *path) {
|
||||
auto wpath = u8string_to_wstring(path);
|
||||
if (wpath.empty()) { return false; }
|
||||
|
||||
hFile_ = ::CreateFile2(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ,
|
||||
OPEN_EXISTING, NULL);
|
||||
hFile_ =
|
||||
::CreateFile2(wpath.c_str(), GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, NULL);
|
||||
|
||||
if (hFile_ == INVALID_HANDLE_VALUE) { return false; }
|
||||
|
||||
@@ -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
|
||||
@@ -5907,56 +5915,50 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
return 0;
|
||||
#elif defined(_GNU_SOURCE) && defined(__GLIBC__) && \
|
||||
(__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2))
|
||||
// Linux implementation using getaddrinfo_a for asynchronous DNS resolution
|
||||
struct gaicb request;
|
||||
// #2431: gai_cancel() is non-blocking and may return EAI_NOTCANCELED while
|
||||
// the resolver worker still references the stack-local gaicb. The cancel
|
||||
// path therefore waits (gai_suspend with no timeout) for the worker to
|
||||
// actually finish before letting the stack frame go. The trade-off is that
|
||||
// a wedged DNS server can hold this thread for the system resolver timeout
|
||||
// (~30s by default) past the caller's connection timeout.
|
||||
struct gaicb request {};
|
||||
struct gaicb *requests[1] = {&request};
|
||||
struct sigevent sevp;
|
||||
struct timespec timeout;
|
||||
struct sigevent sevp {};
|
||||
struct timespec timeout {
|
||||
timeout_sec, 0
|
||||
};
|
||||
|
||||
// Initialize the request structure
|
||||
memset(&request, 0, sizeof(request));
|
||||
request.ar_name = node;
|
||||
request.ar_service = service;
|
||||
request.ar_request = hints;
|
||||
|
||||
// Set up timeout
|
||||
timeout.tv_sec = timeout_sec;
|
||||
timeout.tv_nsec = 0;
|
||||
|
||||
// Initialize sigevent structure (not used, but required)
|
||||
memset(&sevp, 0, sizeof(sevp));
|
||||
sevp.sigev_notify = SIGEV_NONE;
|
||||
|
||||
// Start asynchronous resolution
|
||||
int start_result = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp);
|
||||
if (start_result != 0) { return start_result; }
|
||||
int rc = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp);
|
||||
if (rc != 0) { return rc; }
|
||||
|
||||
// Wait for completion with timeout
|
||||
int wait_result =
|
||||
gai_suspend((const struct gaicb *const *)requests, 1, &timeout);
|
||||
auto cleanup = scope_exit([&] {
|
||||
if (request.ar_result) { freeaddrinfo(request.ar_result); }
|
||||
});
|
||||
|
||||
int wait_result = gai_suspend(requests, 1, &timeout);
|
||||
|
||||
if (wait_result == 0 || wait_result == EAI_ALLDONE) {
|
||||
// Completed successfully, get the result
|
||||
int gai_result = gai_error(&request);
|
||||
if (gai_result == 0) {
|
||||
*res = request.ar_result;
|
||||
request.ar_result = nullptr;
|
||||
return 0;
|
||||
} else {
|
||||
// Clean up on error
|
||||
if (request.ar_result) { freeaddrinfo(request.ar_result); }
|
||||
return gai_result;
|
||||
}
|
||||
} else if (wait_result == EAI_AGAIN) {
|
||||
// Timeout occurred, cancel the request
|
||||
gai_cancel(&request);
|
||||
return EAI_AGAIN;
|
||||
} else {
|
||||
// Other error occurred
|
||||
gai_cancel(&request);
|
||||
return wait_result;
|
||||
return gai_result;
|
||||
}
|
||||
|
||||
gai_cancel(&request);
|
||||
while (gai_error(&request) == EAI_INPROGRESS) {
|
||||
gai_suspend(requests, 1, nullptr);
|
||||
}
|
||||
return wait_result;
|
||||
#else
|
||||
// Fallback implementation using thread-based timeout for other Unix systems
|
||||
// Fallback implementation using thread-based timeout for other Unix systems.
|
||||
|
||||
struct GetAddrInfoState {
|
||||
~GetAddrInfoState() {
|
||||
@@ -6366,6 +6368,10 @@ inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) {
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive form retained so operator""_t below can compute hashes for
|
||||
// switch-case labels at compile time (C++11 constexpr forbids loops). Do not
|
||||
// call from runtime paths with arbitrary-length inputs — use str2tag()
|
||||
// instead, which is iterative and stack-safe.
|
||||
inline constexpr unsigned int str2tag_core(const char *s, size_t l,
|
||||
unsigned int h) {
|
||||
return (l == 0)
|
||||
@@ -6379,7 +6385,16 @@ inline constexpr unsigned int str2tag_core(const char *s, size_t l,
|
||||
}
|
||||
|
||||
inline unsigned int str2tag(const std::string &s) {
|
||||
return str2tag_core(s.data(), s.size(), 0);
|
||||
// Iterative form of str2tag_core: the recursive constexpr version is kept
|
||||
// for compile-time UDL evaluation of short string literals, but at runtime
|
||||
// we may receive arbitrarily long inputs (e.g. fuzzed Content-Type) that
|
||||
// would blow the stack with one frame per character.
|
||||
unsigned int h = 0;
|
||||
for (auto c : s) {
|
||||
h = (((std::numeric_limits<unsigned int>::max)() >> 6) & h * 33) ^
|
||||
static_cast<unsigned char>(c);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
namespace udl {
|
||||
@@ -8571,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
|
||||
@@ -10039,9 +10061,29 @@ inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr)
|
||||
#endif
|
||||
max_thread_count_ = max_n == 0 ? n : max_n;
|
||||
threads_.reserve(base_thread_count_);
|
||||
for (size_t i = 0; i < base_thread_count_; i++) {
|
||||
threads_.emplace_back(std::thread([this]() { worker(false); }));
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
try {
|
||||
#endif
|
||||
for (size_t i = 0; i < base_thread_count_; i++) {
|
||||
threads_.emplace_back(std::thread([this]() { worker(false); }));
|
||||
}
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
} catch (...) {
|
||||
// If thread creation fails partway (e.g., pthread_create returns EAGAIN),
|
||||
// signal the workers we already spawned to exit and join them so the
|
||||
// vector destructor does not see joinable threads (which would call
|
||||
// std::terminate). Then rethrow so the caller learns of the failure.
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
shutdown_ = true;
|
||||
}
|
||||
cond_.notify_all();
|
||||
for (auto &t : threads_) {
|
||||
if (t.joinable()) { t.join(); }
|
||||
}
|
||||
throw;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool ThreadPool::enqueue(std::function<void()> fn) {
|
||||
@@ -10970,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));
|
||||
@@ -11277,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;
|
||||
}
|
||||
@@ -11861,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];
|
||||
|
||||
@@ -11951,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;
|
||||
}
|
||||
@@ -12153,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;
|
||||
}
|
||||
}
|
||||
@@ -12827,10 +12863,22 @@ inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,
|
||||
stream_line_reader lr(strm, line_buf, sizeof(line_buf));
|
||||
if (!lr.getline()) { return -1; }
|
||||
|
||||
char *endptr = nullptr;
|
||||
unsigned long chunk_len = std::strtoul(lr.ptr(), &endptr, 16);
|
||||
if (endptr == lr.ptr()) { return -1; }
|
||||
if (chunk_len == ULONG_MAX) { return -1; }
|
||||
// RFC 9112 §7.1: chunk-size = 1*HEXDIG
|
||||
const char *p = lr.ptr();
|
||||
int v = 0;
|
||||
if (!is_hex(*p, v)) { return -1; }
|
||||
|
||||
size_t chunk_len = 0;
|
||||
constexpr size_t chunk_len_max = (std::numeric_limits<size_t>::max)();
|
||||
for (; is_hex(*p, v); ++p) {
|
||||
if (chunk_len > (chunk_len_max >> 4)) { return -1; }
|
||||
chunk_len = (chunk_len << 4) | static_cast<size_t>(v);
|
||||
}
|
||||
|
||||
while (is_space_or_tab(*p)) {
|
||||
++p;
|
||||
}
|
||||
if (*p != '\0' && *p != ';' && *p != '\r' && *p != '\n') { return -1; }
|
||||
|
||||
if (chunk_len == 0) {
|
||||
chunk_remaining = 0;
|
||||
@@ -12840,7 +12888,7 @@ inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,
|
||||
return 0;
|
||||
}
|
||||
|
||||
chunk_remaining = static_cast<size_t>(chunk_len);
|
||||
chunk_remaining = chunk_len;
|
||||
last_chunk_total = chunk_remaining;
|
||||
last_chunk_offset = 0;
|
||||
}
|
||||
@@ -13637,7 +13685,15 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
output_error_log(error, &req);
|
||||
return false;
|
||||
}
|
||||
res.body.reserve(static_cast<size_t>(len));
|
||||
// Cap the reservation by payload_max_length_ to avoid OOM when a
|
||||
// hostile or malformed server sends an enormous Content-Length.
|
||||
// The actual body read below is bounded by payload_max_length_,
|
||||
// so reserving more than that is never useful.
|
||||
auto reserve_len = static_cast<size_t>(len);
|
||||
if (payload_max_length_ > 0 && reserve_len > payload_max_length_) {
|
||||
reserve_len = payload_max_length_;
|
||||
}
|
||||
res.body.reserve(reserve_len);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16097,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 =
|
||||
@@ -16462,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;
|
||||
@@ -17997,6 +18070,9 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
|
||||
err.code = impl::map_mbedtls_error(ret, err.sys_errno);
|
||||
err.backend_code = static_cast<uint64_t>(-ret);
|
||||
impl::mbedtls_last_error() = ret;
|
||||
// mbedTLS signals a clean close_notify via a negative error code rather
|
||||
// than 0; surface it as a clean EOF the way OpenSSL/wolfSSL do.
|
||||
if (err.code == ErrorCode::PeerClosed) { return 0; }
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
+33
-14
@@ -2,10 +2,12 @@
|
||||
#
|
||||
# Release a new version of cpp-httplib.
|
||||
#
|
||||
# Usage: ./release.sh [--run]
|
||||
# Usage: ./release.sh [--run] [--minor]
|
||||
#
|
||||
# By default, runs in dry-run mode (no changes made).
|
||||
# Pass --run to actually update files, commit, tag, and push.
|
||||
# Pass --minor to force a minor bump even when ABI is unchanged
|
||||
# (use this for behavioral breaking changes that don't break ABI).
|
||||
#
|
||||
# This script:
|
||||
# 1. Reads the current version from httplib.h
|
||||
@@ -14,21 +16,30 @@
|
||||
# 4. Determines the next version automatically:
|
||||
# - abidiff passed → patch bump (e.g., 0.38.0 → 0.38.1)
|
||||
# - abidiff failed → minor bump (e.g., 0.38.1 → 0.39.0)
|
||||
# - --minor passed → forces minor bump regardless of abidiff
|
||||
# 5. Updates httplib.h and docs-src/config.toml
|
||||
# 6. Commits, tags (vX.Y.Z), and pushes
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DRY_RUN=1
|
||||
if [ "${1:-}" = "--run" ]; then
|
||||
DRY_RUN=0
|
||||
shift
|
||||
fi
|
||||
|
||||
if [ $# -ne 0 ]; then
|
||||
echo "Usage: $0 [--run]"
|
||||
exit 1
|
||||
fi
|
||||
FORCE_MINOR=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--run)
|
||||
DRY_RUN=0
|
||||
shift
|
||||
;;
|
||||
--minor)
|
||||
FORCE_MINOR=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [--run] [--minor]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Step 1: Read current version from httplib.h ---
|
||||
CURRENT_VERSION=$(sed -n 's/^#define CPPHTTPLIB_VERSION "\([^"]*\)"/\1/p' httplib.h)
|
||||
@@ -51,8 +62,7 @@ HEAD_SHORT=$(git rev-parse --short HEAD)
|
||||
echo " Latest commit: $HEAD_SHORT"
|
||||
|
||||
# Fetch all workflow runs for the HEAD commit
|
||||
RUNS=$(gh run list --json name,conclusion,headSha \
|
||||
--jq "[.[] | select(.headSha == \"$HEAD_SHA\")]")
|
||||
RUNS=$(gh run list --commit "$HEAD_SHA" --json name,conclusion,headSha)
|
||||
|
||||
NUM_RUNS=$(echo "$RUNS" | jq 'length')
|
||||
|
||||
@@ -95,7 +105,12 @@ fi
|
||||
echo " All non-abidiff CI checks passed."
|
||||
|
||||
# --- Step 4: Determine new version ---
|
||||
if [ "$ABIDIFF_PASSED" -eq 1 ]; then
|
||||
if [ "$FORCE_MINOR" -eq 1 ] && [ "$ABIDIFF_PASSED" -eq 1 ]; then
|
||||
NEW_MINOR=$((V_MINOR + 1))
|
||||
NEW_VERSION="$V_MAJOR.$NEW_MINOR.0"
|
||||
echo ""
|
||||
echo "==> abidiff passed but --minor specified → forced minor bump"
|
||||
elif [ "$ABIDIFF_PASSED" -eq 1 ]; then
|
||||
NEW_PATCH=$((V_PATCH + 1))
|
||||
NEW_VERSION="$V_MAJOR.$V_MINOR.$NEW_PATCH"
|
||||
echo ""
|
||||
@@ -104,7 +119,11 @@ else
|
||||
NEW_MINOR=$((V_MINOR + 1))
|
||||
NEW_VERSION="$V_MAJOR.$NEW_MINOR.0"
|
||||
echo ""
|
||||
echo "==> abidiff failed → minor bump"
|
||||
if [ "$FORCE_MINOR" -eq 1 ]; then
|
||||
echo "==> abidiff failed → minor bump (--minor also specified)"
|
||||
else
|
||||
echo "==> abidiff failed → minor bump"
|
||||
fi
|
||||
fi
|
||||
|
||||
VERSION_HEX=$(printf "0x%02x%02x%02x" "${NEW_VERSION%%.*}" "$(echo "$NEW_VERSION" | cut -d. -f2)" "${NEW_VERSION##*.}")
|
||||
|
||||
+65
-17
@@ -18,8 +18,10 @@ ifneq ($(OS), Windows_NT)
|
||||
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -lssl -lcrypto
|
||||
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -lmbedtls -lmbedx509 -lmbedcrypto
|
||||
WOLFSSL_SUPPORT = -DCPPHTTPLIB_WOLFSSL_SUPPORT -lwolfssl
|
||||
# Disable ASLR for ASAN compatibility on WSL2 (high-entropy ASLR conflicts with ASAN shadow memory)
|
||||
SETARCH = setarch $(shell uname -m) -R
|
||||
ifeq ($(UNAME_S), Linux)
|
||||
# Disable ASLR for ASAN compatibility on WSL2 (high-entropy ASLR conflicts with ASAN shadow memory)
|
||||
SETARCH = setarch $(shell uname -m) -R
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -67,22 +69,29 @@ SHARDS ?= 4
|
||||
|
||||
define run_parallel
|
||||
@echo "Running $(1) with $(SHARDS) shards in parallel..."
|
||||
@fail=0; \
|
||||
@fail=0; pids=""; \
|
||||
for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
|
||||
GTEST_TOTAL_SHARDS=$(SHARDS) GTEST_SHARD_INDEX=$$i \
|
||||
LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
|
||||
$(SETARCH) ./$(1) --gtest_color=yes > $(1)_shard_$$i.log 2>&1 & \
|
||||
pids="$$pids $$!"; \
|
||||
done; \
|
||||
wait; \
|
||||
for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
|
||||
if ! grep -q "\[ PASSED \]" $(1)_shard_$$i.log; then \
|
||||
echo "=== Shard $$i FAILED ==="; \
|
||||
cat $(1)_shard_$$i.log; \
|
||||
fail=1; \
|
||||
else \
|
||||
passed=$$(grep "\[ PASSED \]" $(1)_shard_$$i.log); \
|
||||
exits=""; \
|
||||
for pid in $$pids; do \
|
||||
wait $$pid; exits="$$exits $$?"; \
|
||||
done; \
|
||||
i=0; \
|
||||
for ec in $$exits; do \
|
||||
log=$(1)_shard_$$i.log; \
|
||||
if grep -q "\[ PASSED \]" $$log && ! grep -q "\[ FAILED \]" $$log && [ $$ec -eq 0 ]; then \
|
||||
passed=$$(grep "\[ PASSED \]" $$log); \
|
||||
echo "Shard $$i: $$passed"; \
|
||||
else \
|
||||
echo "=== Shard $$i FAILED (exit=$$ec) ==="; \
|
||||
cat $$log; \
|
||||
fail=1; \
|
||||
fi; \
|
||||
i=$$((i+1)); \
|
||||
done; \
|
||||
if [ $$fail -ne 0 ]; then exit 1; fi; \
|
||||
echo "All shards passed."
|
||||
@@ -193,8 +202,25 @@ test_split_no_tls : test.cc ../httplib.h httplib.cc Makefile
|
||||
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_NO_TLS)
|
||||
|
||||
# ThreadPool unit tests (no TLS, no compression needed)
|
||||
#
|
||||
# The constructor-exception-safety reproducer test interposes pthread_create
|
||||
# at link time. The link flags below enable that interposition. ASAN is also
|
||||
# stripped from this target because libasan installs its own pthread_create
|
||||
# interceptor; layering our override on top corrupts ASAN's thread bookkeeping
|
||||
# and trips "Joining already joined thread" on Linux. ThreadPool memory
|
||||
# behavior is still covered by the ASAN-instrumented `test` binary.
|
||||
ifneq ($(OS), Windows_NT)
|
||||
ifeq ($(shell uname -s), Darwin)
|
||||
THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,-flat_namespace
|
||||
else
|
||||
THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,--export-dynamic
|
||||
endif
|
||||
endif
|
||||
|
||||
THREAD_POOL_CXXFLAGS := $(filter-out -fsanitize=address,$(CXXFLAGS))
|
||||
|
||||
test_thread_pool : test_thread_pool.cc ../httplib.h Makefile
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread
|
||||
$(CXX) -o $@ -I.. $(THREAD_POOL_CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread $(THREAD_POOL_INTERPOSE_LDFLAGS)
|
||||
|
||||
check_abi:
|
||||
@./check-shared-library-abi-compatibility.sh
|
||||
@@ -244,16 +270,38 @@ test_proxy_mbedtls : test_proxy.cc ../httplib.h Makefile cert.pem
|
||||
test_proxy_wolfssl : test_proxy.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS_WOLFSSL)
|
||||
|
||||
# Runs server_fuzzer.cc based on value of $(LIB_FUZZING_ENGINE).
|
||||
# Usage: make fuzz_test LIB_FUZZING_ENGINE=/path/to/libFuzzer
|
||||
fuzz_test: server_fuzzer
|
||||
./server_fuzzer fuzzing/corpus/*
|
||||
# Runs all fuzz harnesses based on the value of $(LIB_FUZZING_ENGINE).
|
||||
# By default LIB_FUZZING_ENGINE is standalone_fuzz_target_runner.o, so each
|
||||
# fuzzer is replayed over its regression corpus.
|
||||
# Override for actual fuzzing:
|
||||
# make fuzz_test LIB_FUZZING_ENGINE=/path/to/libFuzzer
|
||||
fuzz_test: server_fuzzer client_fuzzer header_parser_fuzzer url_parser_fuzzer
|
||||
@m=""; for f in fuzzing/corpus/[0-9]* fuzzing/corpus/issue1264 fuzzing/corpus/clusterfuzz-testcase-minimized-server_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
|
||||
if [ -n "$$m" ]; then echo "./server_fuzzer$$m"; ./server_fuzzer $$m; else echo "(no server_fuzzer corpus)"; fi
|
||||
@m=""; for f in fuzzing/corpus/clusterfuzz-testcase-minimized-client_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
|
||||
if [ -n "$$m" ]; then echo "./client_fuzzer$$m"; ./client_fuzzer $$m; else echo "(no client_fuzzer corpus)"; fi
|
||||
@m=""; for f in fuzzing/corpus/clusterfuzz-testcase-minimized-header_parser_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
|
||||
if [ -n "$$m" ]; then echo "./header_parser_fuzzer$$m"; ./header_parser_fuzzer $$m; else echo "(no header_parser_fuzzer corpus)"; fi
|
||||
@m=""; for f in fuzzing/corpus/clusterfuzz-testcase-minimized-url_parser_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
|
||||
if [ -n "$$m" ]; then echo "./url_parser_fuzzer$$m"; ./url_parser_fuzzer $$m; else echo "(no url_parser_fuzzer corpus)"; fi
|
||||
|
||||
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
||||
server_fuzzer : fuzzing/server_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
|
||||
@file $@
|
||||
|
||||
client_fuzzer : fuzzing/client_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
|
||||
@file $@
|
||||
|
||||
header_parser_fuzzer : fuzzing/header_parser_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
|
||||
@file $@
|
||||
|
||||
url_parser_fuzzer : fuzzing/url_parser_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
|
||||
@file $@
|
||||
|
||||
# Standalone fuzz runner, which just reads inputs from fuzzing/corpus/ dir and
|
||||
# feeds it to server_fuzzer.
|
||||
standalone_fuzz_target_runner.o : fuzzing/standalone_fuzz_target_runner.cpp
|
||||
@@ -266,5 +314,5 @@ cert.pem:
|
||||
./gen-certs.sh
|
||||
|
||||
clean:
|
||||
rm -rf test test_split test_mbedtls test_split_mbedtls test_wolfssl test_split_wolfssl test_no_tls, test_split_no_tls test_proxy test_proxy_mbedtls test_proxy_wolfssl test_benchmark server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log cpp-httplib
|
||||
rm -rf test test_split test_mbedtls test_split_mbedtls test_wolfssl test_split_wolfssl test_no_tls, test_split_no_tls test_proxy test_proxy_mbedtls test_proxy_wolfssl test_benchmark server_fuzzer client_fuzzer header_parser_fuzzer url_parser_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log cpp-httplib
|
||||
|
||||
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Delayed UDP responder used as a loopback test fixture.
|
||||
|
||||
This is a self-contained test fixture for the GetAddrInfoAsyncCancelTest
|
||||
cases (reproducer for cpp-httplib issue #2431). It is NOT a general-purpose
|
||||
nameserver and is only intended to run on 127.0.0.1 inside the test job's
|
||||
own runner / container.
|
||||
|
||||
What it does
|
||||
------------
|
||||
Binds a UDP socket on 127.0.0.1:<port>, accepts well-formed DNS queries
|
||||
from the test process, waits <delay_seconds>, then sends back a minimal
|
||||
NXDOMAIN reply. The deliberate delay is what makes the bug reproducible:
|
||||
|
||||
* The test calls getaddrinfo_with_timeout() with timeout_sec=1.
|
||||
* gai_suspend() returns EAI_AGAIN after 1s; the function returns and
|
||||
its stack frame is destroyed.
|
||||
* The fixture replies after <delay_seconds> (= 3s by default), so the
|
||||
glibc resolver worker thread receives the response *after* the
|
||||
caller's frame is gone and writes back into freed stack memory.
|
||||
* AddressSanitizer (with detect_stack_use_after_return=1) catches the
|
||||
write and aborts with a stack-use-after-return diagnostic.
|
||||
|
||||
Without this fixture the bug is hard to surface: dropping UDP/53 makes
|
||||
the resolver hang forever, so the worker never receives anything and
|
||||
never reaches the buggy write-back path.
|
||||
|
||||
Usage
|
||||
-----
|
||||
python3 test/dns_test_fixture.py <port> [<delay_seconds>]
|
||||
|
||||
Only standard library; no third-party dependencies.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
def serve(port: int, delay_sec: float) -> None:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(("127.0.0.1", port))
|
||||
print(
|
||||
f"[dns_test_fixture] listening on 127.0.0.1:{port}, "
|
||||
f"reply delay={delay_sec}s",
|
||||
flush=True,
|
||||
)
|
||||
while True:
|
||||
try:
|
||||
data, addr = sock.recvfrom(2048)
|
||||
except OSError:
|
||||
return
|
||||
threading.Thread(
|
||||
target=_reply_after_delay,
|
||||
args=(sock, data, addr, delay_sec),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
|
||||
def _reply_after_delay(sock, query: bytes, addr, delay_sec: float) -> None:
|
||||
time.sleep(delay_sec)
|
||||
if len(query) < 12:
|
||||
return
|
||||
# Header: copy transaction id, set QR=1 RA=1 RCODE=3 (NXDOMAIN),
|
||||
# preserve the requester's RD bit, then echo the question section so
|
||||
# glibc's resolver accepts the reply as matching its outstanding query.
|
||||
txid = query[:2]
|
||||
rd_bit = query[2] & 0x01
|
||||
flags = struct.pack(">H", 0x8003 | (rd_bit << 8))
|
||||
counts = struct.pack(">HHHH", 1, 0, 0, 0)
|
||||
question = query[12:]
|
||||
reply = txid + flags + counts + question
|
||||
try:
|
||||
sock.sendto(reply, addr)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
port_arg = int(sys.argv[1])
|
||||
delay_arg = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0
|
||||
serve(port_arg, delay_arg)
|
||||
@@ -13,7 +13,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
||||
BROTLI_DIR = /usr/local/opt/brotli
|
||||
# BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
|
||||
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer
|
||||
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer
|
||||
|
||||
# Runs all the tests and also fuzz tests against seed corpus.
|
||||
all : $(FUZZERS)
|
||||
@@ -25,6 +25,10 @@ server_fuzzer : server_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
zip -q -r server_fuzzer_seed_corpus.zip corpus
|
||||
|
||||
client_fuzzer : client_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
zip -q -r client_fuzzer_seed_corpus.zip corpus
|
||||
|
||||
header_parser_fuzzer : header_parser_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <httplib.h>
|
||||
|
||||
class FuzzedStream : public httplib::Stream {
|
||||
public:
|
||||
FuzzedStream(const uint8_t *data, size_t size)
|
||||
: data_(data), size_(size), read_pos_(0) {}
|
||||
|
||||
ssize_t read(char *ptr, size_t size) override {
|
||||
if (size + read_pos_ > size_) { size = size_ - read_pos_; }
|
||||
memcpy(ptr, data_ + read_pos_, size);
|
||||
read_pos_ += size;
|
||||
return static_cast<ssize_t>(size);
|
||||
}
|
||||
|
||||
ssize_t write(const char *ptr, size_t size) override {
|
||||
request_.append(ptr, size);
|
||||
return static_cast<ssize_t>(size);
|
||||
}
|
||||
|
||||
ssize_t write(const char *ptr) { return write(ptr, strlen(ptr)); }
|
||||
|
||||
ssize_t write(const std::string &s) { return write(s.data(), s.size()); }
|
||||
|
||||
bool is_readable() const override { return true; }
|
||||
|
||||
bool wait_readable() const override { return true; }
|
||||
|
||||
bool wait_writable() const override { return true; }
|
||||
|
||||
void get_remote_ip_and_port(std::string &ip, int &port) const override {
|
||||
ip = "127.0.0.1";
|
||||
port = 8080;
|
||||
}
|
||||
|
||||
void get_local_ip_and_port(std::string &ip, int &port) const override {
|
||||
ip = "127.0.0.1";
|
||||
port = 8080;
|
||||
}
|
||||
|
||||
socket_t socket() const override { return 0; }
|
||||
|
||||
time_t duration() const override { return 0; };
|
||||
|
||||
private:
|
||||
const uint8_t *data_;
|
||||
size_t size_;
|
||||
size_t read_pos_;
|
||||
std::string request_;
|
||||
};
|
||||
|
||||
class FuzzableClient : public httplib::ClientImpl {
|
||||
public:
|
||||
FuzzableClient() : httplib::ClientImpl("localhost", 8080) {}
|
||||
|
||||
void ProcessFuzzedResponse(FuzzedStream &stream, const std::string &method) {
|
||||
httplib::Request req;
|
||||
req.method = method;
|
||||
req.path = "/";
|
||||
httplib::Response res;
|
||||
bool close_connection = false;
|
||||
httplib::Error error = httplib::Error::Success;
|
||||
|
||||
process_request(stream, req, res, close_connection, error);
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
if (size < 1) return 0;
|
||||
|
||||
FuzzedStream stream{data + 1, size - 1};
|
||||
FuzzableClient client;
|
||||
|
||||
// Use the first byte to select method
|
||||
std::string method;
|
||||
switch (data[0] % 6) {
|
||||
case 0: method = "GET"; break;
|
||||
case 1: method = "POST"; break;
|
||||
case 2: method = "PUT"; break;
|
||||
case 3: method = "PATCH"; break;
|
||||
case 4: method = "DELETE"; break;
|
||||
case 5: method = "OPTIONS"; break;
|
||||
}
|
||||
|
||||
client.ProcessFuzzedResponse(stream, method);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
HTTP/1.1 777
|
||||
Content-Length:20000000000
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
HTTP/1.1 777
|
||||
Content-Length:446744071854775
|
||||
|
||||
BIN
Binary file not shown.
@@ -1,3 +1,7 @@
|
||||
# OpenSSL 3.x internal caches (provider, cipher, keymgmt) are allocated
|
||||
# lazily and intentionally kept until process exit. These are not real leaks.
|
||||
leak:libcrypto
|
||||
|
||||
# wolfSSL keeps ECC point/scratch buffers alive across handshakes; they are
|
||||
# released only at library shutdown which the test binaries do not invoke.
|
||||
leak:libwolfssl
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproducer runner for Issue #2431
|
||||
# (https://github.com/yhirose/cpp-httplib/issues/2431).
|
||||
#
|
||||
# Spins up an Ubuntu container, runs the loopback DNS test fixture
|
||||
# (test/dns_test_fixture.py), routes the container's DNS lookups to
|
||||
# that fixture via an iptables NAT rule, builds the test suite with
|
||||
# g++ + ASAN, and runs the GetAddrInfoAsyncCancelTest cases.
|
||||
#
|
||||
# Expected outcomes:
|
||||
# - HEAD prior to the fix: ASAN reports stack-use-after-return inside
|
||||
# getaddrinfo_with_timeout's getaddrinfo_a path during one of the
|
||||
# GetAddrInfoAsyncCancelTest cases.
|
||||
# - HEAD with the fix applied: all three cases PASS.
|
||||
#
|
||||
# Usage:
|
||||
# bash test/run_issue_2431_repro.sh
|
||||
#
|
||||
# Requirements: Docker (Linux container support). The container needs
|
||||
# --privileged because the test binary uses `setarch -R` to disable ASLR
|
||||
# for ASAN compatibility, and because the test job manages iptables
|
||||
# rules inside the container.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
docker run --rm --privileged \
|
||||
-v "$REPO_ROOT:/work" \
|
||||
-w /work/test \
|
||||
ubuntu:24.04 bash -c '
|
||||
set -euo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq --no-install-recommends \
|
||||
ca-certificates g++ make pkg-config iptables iproute2 util-linux coreutils file \
|
||||
python3 \
|
||||
libssl-dev zlib1g-dev libbrotli-dev libzstd-dev libcurl4-openssl-dev \
|
||||
>/dev/null
|
||||
|
||||
# Force DNS-only resolution: Ubuntu defaults nsswitch.conf to
|
||||
# "hosts: files mdns4_minimal [NOTFOUND=return] dns ...", which
|
||||
# short-circuits to NOTFOUND before reaching glibc DNS code, so the
|
||||
# gai_cancel() branch never gets exercised.
|
||||
sed -i "s/^hosts:.*/hosts: dns/" /etc/nsswitch.conf
|
||||
|
||||
# Start the loopback DNS test fixture (delayed UDP responder).
|
||||
DNS_FIXTURE_PORT=15353
|
||||
DNS_FIXTURE_DELAY=3
|
||||
python3 /work/test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \
|
||||
>/tmp/dns_fixture.log 2>&1 &
|
||||
FIXTURE_PID=$!
|
||||
|
||||
# Route the container DNS lookups to the fixture; conntrack handles the
|
||||
# reply path automatically. /etc/resolv.conf is left untouched.
|
||||
iptables -t nat -I OUTPUT -p udp --dport 53 \
|
||||
-j REDIRECT --to-port "$DNS_FIXTURE_PORT"
|
||||
|
||||
trap '"'"'iptables -t nat -F OUTPUT 2>/dev/null || true; kill "$FIXTURE_PID" 2>/dev/null || true'"'"' EXIT
|
||||
|
||||
# Wait for the fixture to start listening.
|
||||
for _ in $(seq 1 50); do
|
||||
if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" || {
|
||||
echo "ERROR: dns_test_fixture failed to start" >&2
|
||||
cat /tmp/dns_fixture.log >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Sanity check: a DNS lookup must take at least the fixture delay
|
||||
# (proving the NAT rule routes the query to the fixture).
|
||||
start=$(date +%s)
|
||||
getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
if [ "$elapsed" -lt 2 ]; then
|
||||
echo "ERROR: lookup returned in ${elapsed}s; fixture not in DNS path" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)"
|
||||
|
||||
cd /work/test
|
||||
echo "=== building test binary (g++ + ASAN) ==="
|
||||
make CXX=g++ test 2>&1 | tail -5
|
||||
|
||||
ARCH=$(uname -m)
|
||||
echo "=== running GetAddrInfoAsyncCancelTest with CPPHTTPLIB_TEST_ISSUE_2431=1 ==="
|
||||
set +e
|
||||
CPPHTTPLIB_TEST_ISSUE_2431=1 \
|
||||
ASAN_OPTIONS=detect_stack_use_after_return=1 \
|
||||
setarch "$ARCH" -R \
|
||||
./test --gtest_filter="GetAddrInfoAsyncCancelTest.*" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
echo "=== test exit: $rc ==="
|
||||
exit $rc
|
||||
'
|
||||
+504
-2
@@ -1549,6 +1549,140 @@ TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(8));
|
||||
}
|
||||
|
||||
#if defined(__linux__) && defined(__GLIBC__) && \
|
||||
defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO)
|
||||
|
||||
// Forward declaration: in split builds split.py strips `inline` and moves the
|
||||
// definition into httplib.cc, so detail::getaddrinfo_with_timeout is not
|
||||
// visible from the public httplib.h. Re-declaring it here lets the tests link
|
||||
// against the symbol in both header-only and split builds.
|
||||
namespace httplib {
|
||||
namespace detail {
|
||||
int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
const struct addrinfo *hints,
|
||||
struct addrinfo **res, time_t timeout_sec);
|
||||
} // namespace detail
|
||||
} // namespace httplib
|
||||
|
||||
// Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
|
||||
//
|
||||
// On Linux/glibc, getaddrinfo_with_timeout() runs the lookup via
|
||||
// getaddrinfo_a(GAI_NOWAIT) using a stack-local `struct gaicb`. When the
|
||||
// gai_suspend() call hits the connection timeout the function calls
|
||||
// gai_cancel() and returns immediately. gai_cancel() is non-blocking and
|
||||
// can return EAI_NOTCANCELED, in which case the resolver worker thread is
|
||||
// still alive and still references the now-destroyed stack frame.
|
||||
//
|
||||
// Triggering the bug requires DNS to actually hang (UDP/53 dropped, etc.),
|
||||
// so these tests are gated on CPPHTTPLIB_TEST_ISSUE_2431=1 and are skipped
|
||||
// during normal runs. test/run_issue_2431_repro.sh sets up the environment
|
||||
// and runs them in a container.
|
||||
namespace {
|
||||
bool should_run_issue_2431_tests() {
|
||||
const char *v = getenv("CPPHTTPLIB_TEST_ISSUE_2431");
|
||||
return v && *v && std::string(v) != "0";
|
||||
}
|
||||
|
||||
std::string unique_unresolvable_host(int n) {
|
||||
// .invalid is reserved (RFC 6761) and is never served by real DNS, but
|
||||
// glibc still asks the configured nameserver — which is exactly the path
|
||||
// we want to exercise. A unique label per call avoids the resolver cache.
|
||||
auto t = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
return "h-" + std::to_string(::getpid()) + "-" + std::to_string(t) + "-" +
|
||||
std::to_string(n) + ".invalid";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(GetAddrInfoAsyncCancelTest, DirectCallSingleThread) {
|
||||
if (!should_run_issue_2431_tests()) {
|
||||
GTEST_SKIP()
|
||||
<< "Set CPPHTTPLIB_TEST_ISSUE_2431=1 (and sinkhole DNS) to run";
|
||||
}
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
struct addrinfo hints;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
auto host = unique_unresolvable_host(i);
|
||||
struct addrinfo *result = nullptr;
|
||||
int rc = detail::getaddrinfo_with_timeout(host.c_str(), "80", &hints,
|
||||
&result, /*timeout_sec=*/1);
|
||||
if (rc == 0 && result) { freeaddrinfo(result); }
|
||||
}
|
||||
|
||||
// Give orphaned getaddrinfo_a worker threads a chance to write into the
|
||||
// stack region they still believe holds their gaicb.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
}
|
||||
|
||||
TEST(GetAddrInfoAsyncCancelTest, DirectCallMultiThread) {
|
||||
if (!should_run_issue_2431_tests()) {
|
||||
GTEST_SKIP()
|
||||
<< "Set CPPHTTPLIB_TEST_ISSUE_2431=1 (and sinkhole DNS) to run";
|
||||
}
|
||||
|
||||
std::atomic<bool> stop{false};
|
||||
std::vector<std::thread> threads;
|
||||
for (int t = 0; t < 8; ++t) {
|
||||
threads.emplace_back([t, &stop] {
|
||||
int i = 0;
|
||||
while (!stop.load(std::memory_order_relaxed)) {
|
||||
struct addrinfo hints;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
auto host = unique_unresolvable_host(t * 100000 + i++);
|
||||
struct addrinfo *result = nullptr;
|
||||
int rc = detail::getaddrinfo_with_timeout(host.c_str(), "80", &hints,
|
||||
&result, /*timeout_sec=*/1);
|
||||
if (rc == 0 && result) { freeaddrinfo(result); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::seconds(8));
|
||||
stop.store(true, std::memory_order_relaxed);
|
||||
for (auto &th : threads) {
|
||||
th.join();
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
}
|
||||
|
||||
TEST(GetAddrInfoAsyncCancelTest, ClientGetMultiThread) {
|
||||
if (!should_run_issue_2431_tests()) {
|
||||
GTEST_SKIP()
|
||||
<< "Set CPPHTTPLIB_TEST_ISSUE_2431=1 (and sinkhole DNS) to run";
|
||||
}
|
||||
|
||||
std::atomic<bool> stop{false};
|
||||
std::vector<std::thread> threads;
|
||||
for (int t = 0; t < 8; ++t) {
|
||||
threads.emplace_back([t, &stop] {
|
||||
int i = 0;
|
||||
while (!stop.load(std::memory_order_relaxed)) {
|
||||
auto host = unique_unresolvable_host(t * 100000 + i++);
|
||||
Client cli(host, 80);
|
||||
cli.set_connection_timeout(1, 0);
|
||||
cli.set_read_timeout(1, 0);
|
||||
cli.set_write_timeout(1, 0);
|
||||
(void)cli.Get("/");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::seconds(8));
|
||||
stop.store(true, std::memory_order_relaxed);
|
||||
for (auto &th : threads) {
|
||||
th.join();
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
}
|
||||
|
||||
#endif // __linux__ && __GLIBC__ && CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO
|
||||
|
||||
TEST(ConnectionErrorTest, InvalidHost) {
|
||||
auto host = "-abcde.com";
|
||||
|
||||
@@ -5012,6 +5146,39 @@ TEST_F(ServerTest, CaseInsensitiveTransferEncoding) {
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
// GHSA-h6wq-j5mv-f3q8: the server must reject malformed chunk-size lines
|
||||
// rather than treat them as valid lengths.
|
||||
template <typename ClientT>
|
||||
static void expect_chunked_body_rejected(ClientT &cli, const char *body) {
|
||||
Request req;
|
||||
req.method = "POST";
|
||||
req.path = "/chunked";
|
||||
|
||||
std::string host_and_port;
|
||||
host_and_port += HOST;
|
||||
host_and_port += ":";
|
||||
host_and_port += std::to_string(PORT);
|
||||
|
||||
req.headers.emplace("Host", host_and_port.c_str());
|
||||
req.headers.emplace("Content-Length", "0");
|
||||
req.headers.emplace("Transfer-Encoding", "chunked");
|
||||
req.body = body;
|
||||
|
||||
auto res = std::make_shared<Response>();
|
||||
auto error = Error::Success;
|
||||
ASSERT_TRUE(cli.send(req, *res, error));
|
||||
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, RejectsNegativeChunkSize) {
|
||||
expect_chunked_body_rejected(cli_, "-2\r\nAAAA\r\n0\r\n\r\n");
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, RejectsChunkSizeWithLeadingPlus) {
|
||||
expect_chunked_body_rejected(
|
||||
cli_, "+4\r\ndech\r\nf\r\nunked post body\r\n0\r\n\r\n");
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, GetStreamed2) {
|
||||
auto res = cli_.Get("/streamed", {{make_range_header({{2, 3}})}});
|
||||
ASSERT_TRUE(res);
|
||||
@@ -7274,6 +7441,122 @@ TEST(ServerRequestParsingTest, EmptyFieldValue) {
|
||||
EXPECT_EQ("HTTP/1.1 200 OK", out.substr(0, 15));
|
||||
}
|
||||
|
||||
TEST(ServerRequestParsingTest, HeaderValueNotPercentDecoded) {
|
||||
Server svr;
|
||||
std::string x_custom;
|
||||
std::string cookie;
|
||||
std::string xff;
|
||||
std::string x_unicode;
|
||||
std::string x_iis;
|
||||
|
||||
svr.Get("/check", [&](const Request &req, Response &res) {
|
||||
x_custom = req.get_header_value("X-Custom");
|
||||
cookie = req.get_header_value("Cookie");
|
||||
xff = req.get_header_value("X-Forwarded-For");
|
||||
x_unicode = req.get_header_value("X-Unicode");
|
||||
x_iis = req.get_header_value("X-IIS");
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
|
||||
thread t = thread([&] { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
const std::string req = "GET /check HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"X-Custom: a%0D%0AInjected: b\r\n"
|
||||
"Cookie: session%3Dvictim%3B%20admin%3Dyes\r\n"
|
||||
"X-Forwarded-For: 1.2.3.4%2C5.6.7.8\r\n"
|
||||
"X-Unicode: %E3%81%82\r\n"
|
||||
"X-IIS: %u00E9\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
std::string res;
|
||||
ASSERT_TRUE(send_request(5, req, &res));
|
||||
EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
|
||||
|
||||
// Every value must be returned verbatim (wire form), with no decoding.
|
||||
EXPECT_EQ("a%0D%0AInjected: b", x_custom);
|
||||
EXPECT_EQ("session%3Dvictim%3B%20admin%3Dyes", cookie);
|
||||
EXPECT_EQ("1.2.3.4%2C5.6.7.8", xff);
|
||||
EXPECT_EQ("%E3%81%82", x_unicode);
|
||||
EXPECT_EQ("%u00E9", x_iis);
|
||||
}
|
||||
|
||||
// Applications that previously relied on automatic percent-decoding can
|
||||
// reproduce the old behavior by explicitly calling decode_path_component()
|
||||
// or, for RFC 3986 conformance, decode_uri_component().
|
||||
TEST(ServerRequestParsingTest, HeaderValueExplicitDecodingByApplication) {
|
||||
Server svr;
|
||||
std::string decoded;
|
||||
|
||||
svr.Get("/check", [&](const Request &req, Response &res) {
|
||||
decoded = decode_uri_component(req.get_header_value("X-Custom"));
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
|
||||
thread t = thread([&] { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
const std::string req = "GET /check HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"X-Custom: hello%20world\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
std::string res;
|
||||
ASSERT_TRUE(send_request(5, req, &res));
|
||||
EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
|
||||
EXPECT_EQ("hello world", decoded);
|
||||
}
|
||||
|
||||
// Regression test for #2033. Browsers send Referer values that include
|
||||
// percent-encoded characters such as %0A inside the URL. Decoding the
|
||||
// header value would either trip the post-decode CR/LF/NUL guard (the
|
||||
// original bug, returning 400) or, after that guard was relaxed, silently
|
||||
// store a literal LF — both unacceptable. The wire form must round-trip.
|
||||
TEST(ServerRequestParsingTest, RefererWithPercentEncodedNewline) {
|
||||
Server svr;
|
||||
std::string referer;
|
||||
|
||||
svr.Get("/check", [&](const Request &req, Response &res) {
|
||||
referer = req.get_header_value("Referer");
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
|
||||
thread t = thread([&] { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
const std::string req = "GET /check HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Referer: http://localhost:1111/?q=Hello%0A\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
std::string res;
|
||||
ASSERT_TRUE(send_request(5, req, &res));
|
||||
EXPECT_EQ("HTTP/1.1 200 OK", res.substr(0, 15));
|
||||
EXPECT_EQ("http://localhost:1111/?q=Hello%0A", referer);
|
||||
}
|
||||
|
||||
TEST(ServerStopTest, StopServerWithChunkedTransmission) {
|
||||
Server svr;
|
||||
|
||||
@@ -7805,6 +8088,31 @@ TEST(MountTest, MultibytesPathName) {
|
||||
EXPECT_EQ(U8("日本語コンテンツ"), res->body);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Issue #2435: mmap::open() must succeed even when another handle holds
|
||||
// the file open for writing (e.g. an active log file).
|
||||
TEST(MmapTest, OpenWhileFileHeldForWriting) {
|
||||
const char *path = "mmap_concurrent_writer_test.txt";
|
||||
const char *content = "hello";
|
||||
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
f.write(content, static_cast<std::streamsize>(strlen(content)));
|
||||
}
|
||||
auto file_cleanup = detail::scope_exit([&] { std::remove(path); });
|
||||
|
||||
HANDLE writer = ::CreateFileA(path, GENERIC_WRITE, FILE_SHARE_READ, NULL,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
ASSERT_NE(INVALID_HANDLE_VALUE, writer);
|
||||
auto handle_cleanup = detail::scope_exit([&] { ::CloseHandle(writer); });
|
||||
|
||||
detail::mmap m(path);
|
||||
ASSERT_TRUE(m.is_open());
|
||||
EXPECT_EQ(strlen(content), m.size());
|
||||
EXPECT_EQ(0, std::memcmp(content, m.data(), strlen(content)));
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(KeepAliveTest, ReadTimeout) {
|
||||
Server svr;
|
||||
|
||||
@@ -9129,6 +9437,81 @@ TEST(ClientVulnerabilityTest, PayloadMaxLengthZeroMeansNoLimit) {
|
||||
<< " bytes without truncation, but only read " << total_read << " bytes.";
|
||||
}
|
||||
|
||||
// Regression test for OSS-Fuzz issue 508342856: a malicious server sending an
|
||||
// enormous Content-Length must not cause the client to pre-allocate a huge
|
||||
// response body buffer. The reservation is capped at payload_max_length_, and
|
||||
// the read itself fails when the body exceeds the limit.
|
||||
TEST(ClientVulnerabilityTest, HugeContentLengthDoesNotPreallocate) {
|
||||
#ifndef _WIN32
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
auto server_thread = std::thread([] {
|
||||
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
default_socket_options(srv);
|
||||
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
|
||||
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_SNDTIMEO, 5, 0);
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 2));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR,
|
||||
#ifdef _WIN32
|
||||
reinterpret_cast<const char *>(&opt),
|
||||
#else
|
||||
&opt,
|
||||
#endif
|
||||
sizeof(opt));
|
||||
|
||||
::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
|
||||
::listen(srv, 1);
|
||||
|
||||
sockaddr_in cli_addr{};
|
||||
socklen_t cli_len = sizeof(cli_addr);
|
||||
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
|
||||
|
||||
if (cli != INVALID_SOCKET) {
|
||||
char buf[4096];
|
||||
::recv(cli, buf, sizeof(buf), 0);
|
||||
|
||||
// Malicious response: claim a 20GB body but send only a tiny payload.
|
||||
std::string response = "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Length: 20000000000\r\n"
|
||||
"\r\n"
|
||||
"abc";
|
||||
::send(cli,
|
||||
#ifdef _WIN32
|
||||
static_cast<const char *>(response.c_str()),
|
||||
static_cast<int>(response.size()),
|
||||
#else
|
||||
response.c_str(), response.size(),
|
||||
#endif
|
||||
0);
|
||||
|
||||
detail::close_socket(cli);
|
||||
}
|
||||
detail::close_socket(srv);
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
{
|
||||
Client cli("127.0.0.1", PORT + 2);
|
||||
cli.set_read_timeout(5, 0);
|
||||
// Default payload_max_length_ is 100MB; a 20GB Content-Length must not
|
||||
// result in a 20GB pre-allocation. The Get() call is expected to fail
|
||||
// (server claims more bytes than payload_max_length permits), but it must
|
||||
// not exhaust memory before getting there.
|
||||
auto res = cli.Get("/malicious");
|
||||
EXPECT_FALSE(res); // Read fails because body exceeds payload_max_length_
|
||||
}
|
||||
|
||||
server_thread.join();
|
||||
}
|
||||
|
||||
// Verify that content_receiver bypasses the default payload_max_length,
|
||||
// allowing streaming downloads larger than 100MB without requiring an explicit
|
||||
// set_payload_max_length call.
|
||||
@@ -10290,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)
|
||||
@@ -12142,7 +12537,7 @@ TEST(MultipartFormDataTest, ManyItemsEndToEnd) {
|
||||
TEST(MultipartFormDataTest, MakeFileProvider) {
|
||||
// Verify make_file_provider sends a file's contents correctly.
|
||||
const std::string file_content(4096, 'Z');
|
||||
const std::string tmp_path = "/tmp/httplib_test_make_file_provider.bin";
|
||||
const std::string tmp_path = "./httplib_test_make_file_provider.bin";
|
||||
{
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
ofs.write(file_content.data(),
|
||||
@@ -12197,7 +12592,7 @@ TEST(MultipartFormDataTest, MakeFileProvider) {
|
||||
|
||||
TEST(MakeFileBodyTest, Basic) {
|
||||
const std::string file_content(4096, 'Z');
|
||||
const std::string tmp_path = "/tmp/httplib_test_make_file_body.bin";
|
||||
const std::string tmp_path = "./httplib_test_make_file_body.bin";
|
||||
{
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
ofs.write(file_content.data(),
|
||||
@@ -13868,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;
|
||||
@@ -17774,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());
|
||||
}
|
||||
|
||||
@@ -291,10 +291,12 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
|
||||
{
|
||||
auto res = cli.Get("/get");
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
{
|
||||
auto res = cli.Get("/redirect/2");
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@@ -306,6 +308,7 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
|
||||
for (auto path : paths) {
|
||||
auto res = cli.Get(path.c_str());
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
auto body = normalizeJson(res->body);
|
||||
EXPECT_TRUE(body.find("\"authenticated\":true") != std::string::npos);
|
||||
EXPECT_TRUE(body.find("\"user\":\"hello\"") != std::string::npos);
|
||||
@@ -317,6 +320,7 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
int count = 10;
|
||||
while (count--) {
|
||||
auto res = cli.Get("/get");
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,63 @@ TEST(ThreadPoolTest, InvalidMaxThreadsThrows) {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Issue #2444: ThreadPool constructor must be exception-safe when std::thread
|
||||
// construction fails partway (e.g., pthread_create returns EAGAIN under thread
|
||||
// resource pressure). Without proper handling, the partially-built threads_
|
||||
// vector destroys joinable std::thread objects, calling std::terminate().
|
||||
//
|
||||
// We reproduce the failure portably by interposing pthread_create at link
|
||||
// time: while the counter is armed, the first N calls succeed, the rest
|
||||
// return EAGAIN. This is gated to POSIX + exceptions-enabled builds.
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
#if defined(__unix__) || defined(__APPLE__)
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
|
||||
namespace {
|
||||
// -1 = pass-through (default). >= 0 = number of remaining successful calls
|
||||
// before EAGAIN is returned. Reset to -1 after each test that arms it.
|
||||
std::atomic<int> g_pthread_create_remaining{-1};
|
||||
} // namespace
|
||||
|
||||
extern "C" int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
|
||||
void *(*start_routine)(void *), void *arg) {
|
||||
using fn_t =
|
||||
int (*)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *);
|
||||
static fn_t real = reinterpret_cast<fn_t>(dlsym(RTLD_NEXT, "pthread_create"));
|
||||
|
||||
int n = g_pthread_create_remaining.load(std::memory_order_relaxed);
|
||||
if (n == 0) { return EAGAIN; }
|
||||
if (n > 0) {
|
||||
g_pthread_create_remaining.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
return real(thread, attr, start_routine, arg);
|
||||
}
|
||||
|
||||
TEST(ThreadPoolTest, ConstructorRecoversWhenThreadCreationFails) {
|
||||
// Allow only the first thread to spawn; subsequent pthread_create calls
|
||||
// return EAGAIN, causing std::thread() to throw std::system_error mid-loop.
|
||||
g_pthread_create_remaining.store(1);
|
||||
|
||||
bool caught = false;
|
||||
try {
|
||||
ThreadPool pool(/*n=*/4);
|
||||
(void)pool;
|
||||
} catch (const std::system_error &) { caught = true; } catch (...) {
|
||||
caught = true;
|
||||
}
|
||||
|
||||
// Disarm before any further test runs.
|
||||
g_pthread_create_remaining.store(-1);
|
||||
|
||||
EXPECT_TRUE(caught);
|
||||
}
|
||||
|
||||
#endif // POSIX
|
||||
#endif // CPPHTTPLIB_NO_EXCEPTIONS
|
||||
|
||||
TEST(ThreadPoolTest, EnqueueAfterShutdownReturnsFalse) {
|
||||
ThreadPool pool(2);
|
||||
pool.shutdown();
|
||||
|
||||
Reference in New Issue
Block a user