mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-04 22:34:09 +07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
811dd0b6f2 | ||
|
|
e8e652824b | ||
|
|
fbb031ed85 | ||
|
|
7d5082cc0e | ||
|
|
600d220c84 | ||
|
|
87d62db46b | ||
|
|
a1fdc07f34 | ||
|
|
eb49a304b6 | ||
|
|
a9bfe5914b |
@@ -21,7 +21,8 @@ jobs:
|
|||||||
- name: Build (Win32)
|
- name: Build (Win32)
|
||||||
shell: cmd
|
shell: cmd
|
||||||
run: |
|
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
|
cl /std:c++14 /EHsc /W4 /WX /c /Fo:NUL test\test_32bit_build.cpp
|
||||||
|
|
||||||
test-arm32:
|
test-arm32:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.43.3"
|
version = "0.44.0"
|
||||||
hostname = "https://yhirose.github.io"
|
hostname = "https://yhirose.github.io"
|
||||||
base_path = "/cpp-httplib"
|
base_path = "/cpp-httplib"
|
||||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||||
#define CPPHTTPLIB_HTTPLIB_H
|
#define CPPHTTPLIB_HTTPLIB_H
|
||||||
|
|
||||||
#define CPPHTTPLIB_VERSION "0.43.3"
|
#define CPPHTTPLIB_VERSION "0.44.0"
|
||||||
#define CPPHTTPLIB_VERSION_NUM "0x002b03"
|
#define CPPHTTPLIB_VERSION_NUM "0x002c00"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||||
@@ -5016,12 +5016,11 @@ inline bool parse_header(const char *beg, const char *end, T fn) {
|
|||||||
|
|
||||||
if (!detail::fields::is_field_value(val)) { return false; }
|
if (!detail::fields::is_field_value(val)) { return false; }
|
||||||
|
|
||||||
if (case_ignore::equal(key, "Location") ||
|
// RFC 9110 §5.5: header field values are opaque octets and MUST NOT be
|
||||||
case_ignore::equal(key, "Referer")) {
|
// percent-decoded by the recipient. Applications that need to interpret a
|
||||||
fn(key, val);
|
// value as a URI component should call httplib::decode_uri_component()
|
||||||
} else {
|
// (or decode_path_component()) explicitly.
|
||||||
fn(key, decode_path_component(val));
|
fn(key, val);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -10047,9 +10046,29 @@ inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr)
|
|||||||
#endif
|
#endif
|
||||||
max_thread_count_ = max_n == 0 ? n : max_n;
|
max_thread_count_ = max_n == 0 ? n : max_n;
|
||||||
threads_.reserve(base_thread_count_);
|
threads_.reserve(base_thread_count_);
|
||||||
for (size_t i = 0; i < base_thread_count_; i++) {
|
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||||
threads_.emplace_back(std::thread([this]() { worker(false); }));
|
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) {
|
inline bool ThreadPool::enqueue(std::function<void()> fn) {
|
||||||
@@ -12835,10 +12854,22 @@ inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,
|
|||||||
stream_line_reader lr(strm, line_buf, sizeof(line_buf));
|
stream_line_reader lr(strm, line_buf, sizeof(line_buf));
|
||||||
if (!lr.getline()) { return -1; }
|
if (!lr.getline()) { return -1; }
|
||||||
|
|
||||||
char *endptr = nullptr;
|
// RFC 9112 §7.1: chunk-size = 1*HEXDIG
|
||||||
unsigned long chunk_len = std::strtoul(lr.ptr(), &endptr, 16);
|
const char *p = lr.ptr();
|
||||||
if (endptr == lr.ptr()) { return -1; }
|
int v = 0;
|
||||||
if (chunk_len == ULONG_MAX) { return -1; }
|
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) {
|
if (chunk_len == 0) {
|
||||||
chunk_remaining = 0;
|
chunk_remaining = 0;
|
||||||
@@ -12848,7 +12879,7 @@ inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
chunk_remaining = static_cast<size_t>(chunk_len);
|
chunk_remaining = chunk_len;
|
||||||
last_chunk_total = chunk_remaining;
|
last_chunk_total = chunk_remaining;
|
||||||
last_chunk_offset = 0;
|
last_chunk_offset = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-14
@@ -2,10 +2,12 @@
|
|||||||
#
|
#
|
||||||
# Release a new version of cpp-httplib.
|
# 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).
|
# By default, runs in dry-run mode (no changes made).
|
||||||
# Pass --run to actually update files, commit, tag, and push.
|
# 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:
|
# This script:
|
||||||
# 1. Reads the current version from httplib.h
|
# 1. Reads the current version from httplib.h
|
||||||
@@ -14,21 +16,30 @@
|
|||||||
# 4. Determines the next version automatically:
|
# 4. Determines the next version automatically:
|
||||||
# - abidiff passed → patch bump (e.g., 0.38.0 → 0.38.1)
|
# - abidiff passed → patch bump (e.g., 0.38.0 → 0.38.1)
|
||||||
# - abidiff failed → minor bump (e.g., 0.38.1 → 0.39.0)
|
# - 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
|
# 5. Updates httplib.h and docs-src/config.toml
|
||||||
# 6. Commits, tags (vX.Y.Z), and pushes
|
# 6. Commits, tags (vX.Y.Z), and pushes
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DRY_RUN=1
|
DRY_RUN=1
|
||||||
if [ "${1:-}" = "--run" ]; then
|
FORCE_MINOR=0
|
||||||
DRY_RUN=0
|
while [ $# -gt 0 ]; do
|
||||||
shift
|
case "$1" in
|
||||||
fi
|
--run)
|
||||||
|
DRY_RUN=0
|
||||||
if [ $# -ne 0 ]; then
|
shift
|
||||||
echo "Usage: $0 [--run]"
|
;;
|
||||||
exit 1
|
--minor)
|
||||||
fi
|
FORCE_MINOR=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [--run] [--minor]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# --- Step 1: Read current version from httplib.h ---
|
# --- Step 1: Read current version from httplib.h ---
|
||||||
CURRENT_VERSION=$(sed -n 's/^#define CPPHTTPLIB_VERSION "\([^"]*\)"/\1/p' 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"
|
echo " Latest commit: $HEAD_SHORT"
|
||||||
|
|
||||||
# Fetch all workflow runs for the HEAD commit
|
# Fetch all workflow runs for the HEAD commit
|
||||||
RUNS=$(gh run list --json name,conclusion,headSha \
|
RUNS=$(gh run list --commit "$HEAD_SHA" --json name,conclusion,headSha)
|
||||||
--jq "[.[] | select(.headSha == \"$HEAD_SHA\")]")
|
|
||||||
|
|
||||||
NUM_RUNS=$(echo "$RUNS" | jq 'length')
|
NUM_RUNS=$(echo "$RUNS" | jq 'length')
|
||||||
|
|
||||||
@@ -95,7 +105,12 @@ fi
|
|||||||
echo " All non-abidiff CI checks passed."
|
echo " All non-abidiff CI checks passed."
|
||||||
|
|
||||||
# --- Step 4: Determine new version ---
|
# --- 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_PATCH=$((V_PATCH + 1))
|
||||||
NEW_VERSION="$V_MAJOR.$V_MINOR.$NEW_PATCH"
|
NEW_VERSION="$V_MAJOR.$V_MINOR.$NEW_PATCH"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -104,7 +119,11 @@ else
|
|||||||
NEW_MINOR=$((V_MINOR + 1))
|
NEW_MINOR=$((V_MINOR + 1))
|
||||||
NEW_VERSION="$V_MAJOR.$NEW_MINOR.0"
|
NEW_VERSION="$V_MAJOR.$NEW_MINOR.0"
|
||||||
echo ""
|
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
|
fi
|
||||||
|
|
||||||
VERSION_HEX=$(printf "0x%02x%02x%02x" "${NEW_VERSION%%.*}" "$(echo "$NEW_VERSION" | cut -d. -f2)" "${NEW_VERSION##*.}")
|
VERSION_HEX=$(printf "0x%02x%02x%02x" "${NEW_VERSION%%.*}" "$(echo "$NEW_VERSION" | cut -d. -f2)" "${NEW_VERSION##*.}")
|
||||||
|
|||||||
+22
-3
@@ -18,8 +18,10 @@ ifneq ($(OS), Windows_NT)
|
|||||||
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -lssl -lcrypto
|
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -lssl -lcrypto
|
||||||
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -lmbedtls -lmbedx509 -lmbedcrypto
|
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -lmbedtls -lmbedx509 -lmbedcrypto
|
||||||
WOLFSSL_SUPPORT = -DCPPHTTPLIB_WOLFSSL_SUPPORT -lwolfssl
|
WOLFSSL_SUPPORT = -DCPPHTTPLIB_WOLFSSL_SUPPORT -lwolfssl
|
||||||
# Disable ASLR for ASAN compatibility on WSL2 (high-entropy ASLR conflicts with ASAN shadow memory)
|
ifeq ($(UNAME_S), Linux)
|
||||||
SETARCH = setarch $(shell uname -m) -R
|
# Disable ASLR for ASAN compatibility on WSL2 (high-entropy ASLR conflicts with ASAN shadow memory)
|
||||||
|
SETARCH = setarch $(shell uname -m) -R
|
||||||
|
endif
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@@ -200,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)
|
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_NO_TLS)
|
||||||
|
|
||||||
# ThreadPool unit tests (no TLS, no compression needed)
|
# 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
|
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_abi:
|
||||||
@./check-shared-library-abi-compatibility.sh
|
@./check-shared-library-abi-compatibility.sh
|
||||||
|
|||||||
+149
@@ -5146,6 +5146,39 @@ TEST_F(ServerTest, CaseInsensitiveTransferEncoding) {
|
|||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
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) {
|
TEST_F(ServerTest, GetStreamed2) {
|
||||||
auto res = cli_.Get("/streamed", {{make_range_header({{2, 3}})}});
|
auto res = cli_.Get("/streamed", {{make_range_header({{2, 3}})}});
|
||||||
ASSERT_TRUE(res);
|
ASSERT_TRUE(res);
|
||||||
@@ -7408,6 +7441,122 @@ TEST(ServerRequestParsingTest, EmptyFieldValue) {
|
|||||||
EXPECT_EQ("HTTP/1.1 200 OK", out.substr(0, 15));
|
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) {
|
TEST(ServerStopTest, StopServerWithChunkedTransmission) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
|
|||||||
@@ -291,10 +291,12 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto res = cli.Get("/get");
|
auto res = cli.Get("/get");
|
||||||
|
ASSERT_TRUE(res != nullptr);
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto res = cli.Get("/redirect/2");
|
auto res = cli.Get("/redirect/2");
|
||||||
|
ASSERT_TRUE(res != nullptr);
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,6 +308,7 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
|||||||
|
|
||||||
for (auto path : paths) {
|
for (auto path : paths) {
|
||||||
auto res = cli.Get(path.c_str());
|
auto res = cli.Get(path.c_str());
|
||||||
|
ASSERT_TRUE(res != nullptr);
|
||||||
auto body = normalizeJson(res->body);
|
auto body = normalizeJson(res->body);
|
||||||
EXPECT_TRUE(body.find("\"authenticated\":true") != std::string::npos);
|
EXPECT_TRUE(body.find("\"authenticated\":true") != std::string::npos);
|
||||||
EXPECT_TRUE(body.find("\"user\":\"hello\"") != 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;
|
int count = 10;
|
||||||
while (count--) {
|
while (count--) {
|
||||||
auto res = cli.Get("/get");
|
auto res = cli.Get("/get");
|
||||||
|
ASSERT_TRUE(res != nullptr);
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,6 +198,63 @@ TEST(ThreadPoolTest, InvalidMaxThreadsThrows) {
|
|||||||
}
|
}
|
||||||
#endif
|
#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) {
|
TEST(ThreadPoolTest, EnqueueAfterShutdownReturnsFalse) {
|
||||||
ThreadPool pool(2);
|
ThreadPool pool(2);
|
||||||
pool.shutdown();
|
pool.shutdown();
|
||||||
|
|||||||
Reference in New Issue
Block a user