Compare commits

...

18 Commits

Author SHA1 Message Date
yhirose dc6faf5c17 Release v0.30.2 2026-02-01 19:11:44 -05:00
yhirose 71bb17fb0e Fix problems with CPPHTTPLIB_NO_EXCEPTIONS 2026-02-01 08:43:13 -05:00
yhirose 094bf112bb Fix #2340 2026-01-30 22:01:43 -05:00
ctabor-itracs fbec2a3466 case insensitive hostname validation (fix #2333) (#2337)
* Add Tests for case sensitive hostname verfication

* Modify check_host_name to perform case insensitive comparisons during ssl hostname validation

---------

Co-authored-by: Tabor, Chris <chris.tabor@commscope.com>
2026-01-23 14:58:47 -05:00
Sung Po-Han c3fa06112b Fix set_ca_cert_store() to skip system certs like set_ca_cert_path() (#2335)
Both APIs conceptually do the same thing: "use these CA certs for
verification." However, set_ca_cert_store() falls through to the else
branch in load_certs() where system certs are added to the user's
custom store, defeating the purpose of certificate pinning.

This change makes set_ca_cert_store() behave consistently with
set_ca_cert_path() by checking ca_cert_store_ before loading system
certificates.

Added test to verify system certs are not loaded when custom store is set.

Co-authored-by: Your <you@example.com>
2026-01-22 18:58:25 -05:00
Prajwal B Mehendarkar f73e694f0c timegm api absent in AIX (#2336) 2026-01-22 18:57:23 -05:00
TH 191bfb2ea4 Fix build error when zstd < 1.5.6 lacks zstd::libzstd CMake target (#2334)
Fix #2313
2026-01-22 18:56:34 -05:00
yhirose ad5839f0d1 Add retry logic to BenchmarkTest test on Windows 2026-01-20 18:43:56 -05:00
yhirose 02dfb97fd6 Add Expect: 100-continue support 2026-01-18 22:38:25 -05:00
yhirose a38a076571 Resolve #2262 (#2332)
* Resolve #2262

* Enhance request handling on Windows by adding early response check for large request bodies

* Enhance early response handling for large requests with long URIs on Windows
2026-01-18 00:38:43 -05:00
yhirose 0e1b52b23e Fix #2325 (#2331)
* Fix #2325

* clang-format
2026-01-16 18:19:14 -05:00
yhirose c0469eba96 Revert "Fix #2325"
This reverts commit 7dec57d1eb.
2026-01-16 17:28:28 -05:00
yhirose 7dec57d1eb Fix #2325 2026-01-16 16:25:06 -05:00
yhirose b85aa76bd2 Fix #2321, #2322, #2326 2026-01-16 11:29:09 -05:00
yhirose cea018f2cd Fix #2324 2026-01-11 21:23:15 -05:00
yhirose 1111219f17 Fix #2324 2026-01-10 21:05:30 -05:00
yhirose a7e1d14b15 Fix warning on Windows 2026-01-10 19:23:35 -05:00
yhirose 6eff49e1fb Problem with CI test on Windows without OpenSSL (#2323)
* Fix problem with 'windows without SSL`

* Fix payload limit enforcement for requests without Content-Length on Windows

- Enable MSG_PEEK on Windows (non-SSL builds) to detect payloads without Content-Length
- Only use MSG_PEEK when payload_max_length is set to a finite value to avoid blocking
- Use read_content_without_length for actual size checking to support any payload limit
- Set 413 Payload Too Large status before rejecting oversized requests

This fixes three test cases on Windows:
- RequestWithoutContentLengthOrTransferEncoding (no payload limit)
- NoContentLengthPayloadLimit (8-byte limit)
- NoContentLengthExceeds10MB (10MB limit)

* clang-format
2026-01-10 19:23:24 -05:00
5 changed files with 497 additions and 87 deletions
+5 -1
View File
@@ -156,6 +156,7 @@ jobs:
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake
-DHTTPLIB_TEST=ON
-DHTTPLIB_COMPILE=${{ matrix.config.compiled && 'ON' || 'OFF' }}
-DHTTPLIB_USE_OPENSSL_IF_AVAILABLE=${{ matrix.config.with_ssl && 'ON' || 'OFF' }}
-DHTTPLIB_REQUIRE_ZLIB=ON
-DHTTPLIB_REQUIRE_BROTLI=ON
-DHTTPLIB_REQUIRE_ZSTD=ON
@@ -164,7 +165,10 @@ jobs:
run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
- name: Run tests ${{ matrix.config.name }}
if: ${{ matrix.config.run_tests }}
run: ctest --output-on-failure --test-dir build -C Release
run: ctest --output-on-failure --test-dir build -C Release -E "BenchmarkTest"
- name: Run benchmark tests with retry ${{ matrix.config.name }}
if: ${{ matrix.config.run_tests }}
run: ctest --output-on-failure --test-dir build -C Release -R "BenchmarkTest" --repeat until-pass:5
env:
VCPKG_ROOT: "C:/vcpkg"
+20
View File
@@ -0,0 +1,20 @@
name: No Exceptions Test
on: [push, pull_request]
jobs:
test-no-exceptions:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libssl-dev zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev
- name: Run tests with CPPHTTPLIB_NO_EXCEPTIONS
run: |
cd test && make EXTRA_CXXFLAGS="-fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS"
+5 -4
View File
@@ -185,10 +185,11 @@ elseif(HTTPLIB_USE_BROTLI_IF_AVAILABLE)
set(HTTPLIB_IS_USING_BROTLI ${Brotli_FOUND})
endif()
# NOTE: When using cpp-httplib as a subproject (e.g., via FetchContent), the zstd::libzstd target may not be visible in the parent project scope.
# If you encounter a "target not found" error, see https://github.com/yhirose/cpp-httplib/issues/2313 for a workaround.
# NOTE:
# zstd < 1.5.6 does not provide the CMake imported target `zstd::libzstd`.
# Older versions must be consumed via their pkg-config file.
if(HTTPLIB_REQUIRE_ZSTD)
find_package(zstd)
find_package(zstd 1.5.6 CONFIG)
if(NOT zstd_FOUND)
find_package(PkgConfig REQUIRED)
pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd)
@@ -196,7 +197,7 @@ if(HTTPLIB_REQUIRE_ZSTD)
endif()
set(HTTPLIB_IS_USING_ZSTD TRUE)
elseif(HTTPLIB_USE_ZSTD_IF_AVAILABLE)
find_package(zstd QUIET)
find_package(zstd 1.5.6 CONFIG QUIET)
if(NOT zstd_FOUND)
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
+283 -57
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.30.1"
#define CPPHTTPLIB_VERSION_NUM "0x001E01"
#define CPPHTTPLIB_VERSION "0.30.2"
#define CPPHTTPLIB_VERSION_NUM "0x001E02"
/*
* Platform compatibility check
@@ -98,6 +98,22 @@
#define CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND 0
#endif
#ifndef CPPHTTPLIB_EXPECT_100_THRESHOLD
#define CPPHTTPLIB_EXPECT_100_THRESHOLD 1024
#endif
#ifndef CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND
#define CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND 1000
#endif
#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD
#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD (1024 * 1024)
#endif
#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND
#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND 50
#endif
#ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND
#define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0
#endif
@@ -286,8 +302,10 @@ using socket_t = int;
#include <atomic>
#include <cassert>
#include <cctype>
#include <chrono>
#include <climits>
#include <condition_variable>
#include <cstdlib>
#include <cstring>
#include <errno.h>
#include <exception>
@@ -305,6 +323,7 @@ using socket_t = int;
#include <sstream>
#include <string>
#include <sys/stat.h>
#include <system_error>
#include <thread>
#include <unordered_map>
#include <unordered_set>
@@ -494,6 +513,69 @@ private:
bool execute_on_destruction;
};
// Simple from_chars implementation for integer and double types (C++17
// substitute)
template <typename T> struct from_chars_result {
const char *ptr;
std::errc ec;
};
template <typename T>
inline from_chars_result<T> from_chars(const char *first, const char *last,
T &value, int base = 10) {
value = 0;
const char *p = first;
bool negative = false;
if (p != last && *p == '-') {
negative = true;
++p;
}
if (p == last) { return {first, std::errc::invalid_argument}; }
T result = 0;
for (; p != last; ++p) {
char c = *p;
int digit = -1;
if ('0' <= c && c <= '9') {
digit = c - '0';
} else if ('a' <= c && c <= 'z') {
digit = c - 'a' + 10;
} else if ('A' <= c && c <= 'Z') {
digit = c - 'A' + 10;
} else {
break;
}
if (digit < 0 || digit >= base) { break; }
if (result > ((std::numeric_limits<T>::max)() - digit) / base) {
return {p, std::errc::result_out_of_range};
}
result = result * base + digit;
}
if (p == first || (negative && p == first + 1)) {
return {first, std::errc::invalid_argument};
}
value = negative ? -result : result;
return {p, std::errc{}};
}
// from_chars for double (simple wrapper for strtod)
inline from_chars_result<double> from_chars(const char *first, const char *last,
double &value) {
std::string s(first, last);
char *endptr = nullptr;
errno = 0;
value = std::strtod(s.c_str(), &endptr);
if (endptr == s.c_str()) { return {first, std::errc::invalid_argument}; }
if (errno == ERANGE) {
return {first + (endptr - s.c_str()), std::errc::result_out_of_range};
}
return {first + (endptr - s.c_str()), std::errc{}};
}
} // namespace detail
enum SSLVerifierResponse {
@@ -1848,10 +1930,11 @@ private:
Result send_(Request &&req);
socket_t create_client_socket(Error &error) const;
bool read_response_line(Stream &strm, const Request &req,
Response &res) const;
bool read_response_line(Stream &strm, const Request &req, Response &res,
bool skip_100_continue = true) const;
bool write_request(Stream &strm, Request &req, bool close_connection,
Error &error);
Error &error, bool skip_body = false);
bool write_request_body(Stream &strm, Request &req, Error &error);
void prepare_default_headers(Request &r, bool for_stream,
const std::string &ct);
bool redirect(Request &req, Response &res, Error &error);
@@ -3243,10 +3326,11 @@ private:
msg.id = value;
} else if (field == "retry") {
// Parse retry interval in milliseconds
try {
retry_ms = std::stoi(value);
} catch (...) {
// Invalid retry value, ignore
{
int v = 0;
auto res =
detail::from_chars(value.data(), value.data() + value.size(), v);
if (res.ec == std::errc{}) { retry_ms = v; }
}
}
// Unknown fields are ignored per SSE spec
@@ -3529,6 +3613,8 @@ inline time_t parse_http_date(const std::string &date_str) {
#ifdef _WIN32
return _mkgmtime(&tm_buf);
#elif defined _AIX
return mktime(&tm_buf);
#else
return timegm(&tm_buf);
#endif
@@ -4788,7 +4874,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
// Allocate on the heap, so the resolver thread can keep using the data.
auto state = std::make_shared<GetAddrInfoState>();
state->node = node;
if (node) { state->node = node; }
state->service = service;
state->hints = *hints;
@@ -6308,10 +6394,20 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
return;
}
const auto first =
static_cast<ssize_t>(lhs.empty() ? -1 : std::stoll(lhs));
const auto last =
static_cast<ssize_t>(rhs.empty() ? -1 : std::stoll(rhs));
ssize_t first = -1;
if (!lhs.empty()) {
ssize_t v;
auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v);
if (res.ec == std::errc{}) { first = v; }
}
ssize_t last = -1;
if (!rhs.empty()) {
ssize_t v;
auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v);
if (res.ec == std::errc{}) { last = v; }
}
if ((first == -1 && last == -1) ||
(first != -1 && last != -1 && first > last)) {
all_valid_ranges = false;
@@ -6386,25 +6482,17 @@ inline bool parse_accept_header(const std::string &s,
return;
}
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
{
std::istringstream iss(quality_str);
iss >> accept_entry.quality;
// Check if conversion was successful and entire string was consumed
if (iss.fail() || !iss.eof()) {
double v = 0.0;
auto res = detail::from_chars(
quality_str.data(), quality_str.data() + quality_str.size(), v);
if (res.ec == std::errc{}) {
accept_entry.quality = v;
} else {
has_invalid_entry = true;
return;
}
}
#else
try {
accept_entry.quality = std::stod(quality_str);
} catch (...) {
has_invalid_entry = true;
return;
}
#endif
// Check if quality is in valid range [0.0, 1.0]
if (accept_entry.quality < 0.0 || accept_entry.quality > 1.0) {
has_invalid_entry = true;
@@ -8982,13 +9070,26 @@ inline bool Server::read_content(Stream &strm, Request &req, Response &res) {
strm, req, res,
// Regular
[&](const char *buf, size_t n) {
// Prevent arithmetic overflow when checking sizes.
// Avoid computing (req.body.size() + n) directly because
// adding two unsigned `size_t` values can wrap around and
// produce a small result instead of indicating overflow.
// Instead, check using subtraction: ensure `n` does not
// exceed the remaining capacity `max_size() - size()`.
if (req.body.size() >= req.body.max_size() ||
n > req.body.max_size() - req.body.size()) {
return false;
}
// Limit decompressed body size to payload_max_length_ to protect
// against "zip bomb" attacks where a small compressed payload
// decompresses to a massive size.
if (req.body.size() + n > payload_max_length_ ||
req.body.size() + n > req.body.max_size()) {
if (payload_max_length_ > 0 &&
(req.body.size() >= payload_max_length_ ||
n > payload_max_length_ - req.body.size())) {
return false;
}
req.body.append(buf, n);
return true;
},
@@ -9078,22 +9179,29 @@ inline bool Server::read_content_core(
// oversized request and fail early (causing connection close). For SSL
// builds we cannot reliably peek the decrypted application bytes, so keep
// the original behaviour.
#if !defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(_WIN32)
#if !defined(CPPHTTPLIB_OPENSSL_SUPPORT)
if (!req.has_header("Content-Length") &&
!detail::is_chunked_transfer_encoding(req.headers)) {
socket_t s = strm.socket();
if (s != INVALID_SOCKET) {
// Peek up to payload_max_length_ + 1 bytes. If more than
// payload_max_length_ bytes are pending, reject the request.
size_t to_peek =
(payload_max_length_ > 0)
? (std::min)(payload_max_length_ + 1, static_cast<size_t>(4096))
: 1;
std::vector<char> peekbuf(to_peek);
ssize_t n = ::recv(s, peekbuf.data(), to_peek, MSG_PEEK);
if (n > 0 && static_cast<size_t>(n) > payload_max_length_) {
// Indicate failure so connection will be closed.
return false;
// Only peek if payload_max_length is set to a finite value
if (payload_max_length_ > 0 &&
payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
socket_t s = strm.socket();
if (s != INVALID_SOCKET) {
// Peek to check if there is any pending data
char peekbuf[1];
ssize_t n = ::recv(s, peekbuf, 1, MSG_PEEK);
if (n > 0) {
// There is data, so read it with payload limit enforcement
auto result = detail::read_content_without_length(
strm, payload_max_length_, out);
if (result == detail::ReadContentResult::PayloadTooLarge) {
res.status = StatusCode::PayloadTooLarge_413;
return false;
} else if (result != detail::ReadContentResult::Success) {
return false;
}
return true;
}
}
}
return true;
@@ -10068,7 +10176,8 @@ inline void ClientImpl::close_socket(Socket &socket) {
}
inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
Response &res) const {
Response &res,
bool skip_100_continue) const {
std::array<char, 2048> buf{};
detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
@@ -10089,8 +10198,8 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
res.status = std::stoi(std::string(m[2]));
res.reason = std::string(m[3]);
// Ignore '100 Continue'
while (res.status == StatusCode::Continue_100) {
// Ignore '100 Continue' (only when not using Expect: 100-continue explicitly)
while (skip_100_continue && res.status == StatusCode::Continue_100) {
if (!line_reader.getline()) { return false; } // CRLF
if (!line_reader.getline()) { return false; } // next response line
@@ -10875,7 +10984,8 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
}
inline bool ClientImpl::write_request(Stream &strm, Request &req,
bool close_connection, Error &error) {
bool close_connection, Error &error,
bool skip_body) {
// Prepare additional headers
if (close_connection) {
if (!req.has_header("Connection")) {
@@ -10994,7 +11104,59 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
}
}
// After sending request line and headers, wait briefly for an early server
// response (e.g. 4xx) and avoid sending a potentially large request body
// unnecessarily. This workaround is only enabled on Windows because Unix
// platforms surface write errors (EPIPE) earlier; on Windows kernel send
// buffering can accept large writes even when the peer already responded.
// Check the stream first (which covers SSL via `is_readable()`), then
// fall back to select on the socket. Only perform the wait for very large
// request bodies to avoid interfering with normal small requests and
// reduce side-effects. Poll briefly (up to 50ms as default) for an early
// response. Skip this check when using Expect: 100-continue, as the protocol
// handles early responses properly.
#if defined(_WIN32)
if (!skip_body &&
req.body.size() > CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD &&
req.path.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
auto start = std::chrono::high_resolution_clock::now();
for (;;) {
// Prefer socket-level readiness to avoid SSL_pending() false-positives
// from SSL internals. If the underlying socket is readable, assume an
// early response may be present.
auto sock = strm.socket();
if (sock != INVALID_SOCKET && detail::select_read(sock, 0, 0) > 0) {
return false;
}
// Fallback to stream-level check for non-socket streams or when the
// socket isn't reporting readable. Avoid using `is_readable()` for
// SSL, since `SSL_pending()` may report buffered records that do not
// indicate a complete application-level response yet.
if (!is_ssl() && strm.is_readable()) { return false; }
auto now = std::chrono::high_resolution_clock::now();
auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(now - start)
.count();
if (elapsed >= CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
#endif
// Body
if (skip_body) { return true; }
return write_request_body(strm, req, error);
}
inline bool ClientImpl::write_request_body(Stream &strm, Request &req,
Error &error) {
if (req.body.empty()) {
return write_content_with_provider(strm, req, error);
}
@@ -11170,8 +11332,20 @@ inline void ClientImpl::output_error_log(const Error &err,
inline bool ClientImpl::process_request(Stream &strm, Request &req,
Response &res, bool close_connection,
Error &error) {
// Send request
if (!write_request(strm, req, close_connection, error)) { return false; }
// Auto-add Expect: 100-continue for large bodies
if (CPPHTTPLIB_EXPECT_100_THRESHOLD > 0 && !req.has_header("Expect")) {
auto body_size = req.body.empty() ? req.content_length_ : req.body.size();
if (body_size >= CPPHTTPLIB_EXPECT_100_THRESHOLD) {
req.set_header("Expect", "100-continue");
}
}
// Check for Expect: 100-continue
auto expect_100_continue = req.get_header_value("Expect") == "100-continue";
// Send request (skip body if using Expect: 100-continue)
auto write_request_success =
write_request(strm, req, close_connection, error, expect_100_continue);
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
if (is_ssl()) {
@@ -11186,14 +11360,48 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
}
#endif
// Handle Expect: 100-continue with timeout
if (expect_100_continue && CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND > 0) {
time_t sec = CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND / 1000;
time_t usec = (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND % 1000) * 1000;
auto ret = detail::select_read(strm.socket(), sec, usec);
if (ret <= 0) {
// Timeout or error: send body anyway (server didn't respond in time)
if (!write_request_body(strm, req, error)) { return false; }
expect_100_continue = false; // Switch to normal response handling
}
}
// Receive response and headers
if (!read_response_line(strm, req, res) ||
// When using Expect: 100-continue, don't auto-skip `100 Continue` response
if (!read_response_line(strm, req, res, !expect_100_continue) ||
!detail::read_headers(strm, res.headers)) {
error = Error::Read;
if (write_request_success) { error = Error::Read; }
output_error_log(error, &req);
return false;
}
if (!write_request_success) { return false; }
// Handle Expect: 100-continue response
if (expect_100_continue) {
if (res.status == StatusCode::Continue_100) {
// Server accepted, send the body
if (!write_request_body(strm, req, error)) { return false; }
// Read the actual response
res.headers.clear();
res.body.clear();
if (!read_response_line(strm, req, res) ||
!detail::read_headers(strm, res.headers)) {
error = Error::Read;
output_error_log(error, &req);
return false;
}
}
// If not 100 Continue, server returned an error; proceed with that response
}
// Body
if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" &&
req.method != "CONNECT") {
@@ -12955,7 +13163,7 @@ inline bool SSLClient::load_certs() {
last_openssl_error_ = ERR_get_error();
ret = false;
}
} else {
} else if (!ca_cert_store_) {
auto loaded = false;
#ifdef _WIN32
loaded =
@@ -13202,7 +13410,11 @@ inline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const {
inline bool SSLClient::check_host_name(const char *pattern,
size_t pattern_len) const {
if (host_.size() == pattern_len && host_ == pattern) { return true; }
// Exact match (case-insensitive)
if (host_.size() == pattern_len &&
detail::case_ignore::equal(host_, std::string(pattern, pattern_len))) {
return true;
}
// Wildcard match
// https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484
@@ -13217,9 +13429,23 @@ inline bool SSLClient::check_host_name(const char *pattern,
auto itr = pattern_components.begin();
for (const auto &h : host_components_) {
auto &p = *itr;
if (p != h && p != "*") {
auto partial_match = (p.size() > 0 && p[p.size() - 1] == '*' &&
!p.compare(0, p.size() - 1, h));
if (!httplib::detail::case_ignore::equal(p, h) && p != "*") {
bool partial_match = false;
if (!p.empty() && p[p.size() - 1] == '*') {
const auto prefix_length = p.size() - 1;
if (prefix_length == 0) {
partial_match = true;
} else if (h.size() >= prefix_length) {
partial_match =
std::equal(p.begin(),
p.begin() + static_cast<std::string::difference_type>(
prefix_length),
h.begin(), [](const char ca, const char cb) {
return httplib::detail::case_ignore::to_lower(ca) ==
httplib::detail::case_ignore::to_lower(cb);
});
}
}
if (!partial_match) { return false; }
}
++itr;
+184 -25
View File
@@ -27,6 +27,15 @@
#include <type_traits>
#include <vector>
#if __cplusplus >= 202002L
inline std::string u8_to_string(const char8_t *s) {
return std::string(reinterpret_cast<const char *>(s));
}
#define U8(x) u8_to_string(u8##x)
#else
#define U8(x) u8##x
#endif
#define SERVER_CERT_FILE "./cert.pem"
#define SERVER_CERT2_FILE "./cert2.pem"
#define SERVER_PRIVATE_KEY_FILE "./key.pem"
@@ -73,7 +82,13 @@ FormData &get_file_value(std::vector<FormData> &items, const char *key) {
static void read_file(const std::string &path, std::string &out) {
std::ifstream fs(path, std::ios_base::binary);
if (!fs) throw std::runtime_error("File not found: " + path);
if (!fs) {
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
return;
#else
throw std::runtime_error("File not found: " + path);
#endif
}
fs.seekg(0, std::ios_base::end);
auto size = fs.tellg();
fs.seekg(0);
@@ -372,7 +387,7 @@ TEST(DecodePathTest, PercentCharacter) {
EXPECT_EQ(
decode_path_component(
R"(descrip=Gastos%20%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%B1%C3%91%206)"),
u8"descrip=Gastos áéíóúñÑ 6");
U8("descrip=Gastos áéíóúñÑ 6"));
}
TEST(DecodePathTest, PercentCharacterNUL) {
@@ -430,9 +445,9 @@ TEST(ClientQueryOrder, PreserveOrder) {
}
TEST(EncodeQueryParamTest, TestUTF8Characters) {
string chineseCharacters = u8"中国語";
string russianCharacters = u8"дом";
string brazilianCharacters = u8"óculos";
string chineseCharacters = U8("中国語");
string russianCharacters = U8("дом");
string brazilianCharacters = U8("óculos");
EXPECT_EQ(httplib::encode_uri_component(chineseCharacters),
"%E4%B8%AD%E5%9B%BD%E8%AA%9E");
@@ -457,9 +472,9 @@ TEST(EncodeUriComponentTest, ParseReservedCharactersTest) {
}
TEST(EncodeUriComponentTest, TestUTF8Characters) {
string chineseCharacters = u8"中国語";
string russianCharacters = u8"дом";
string brazilianCharacters = u8"óculos";
string chineseCharacters = U8("中国語");
string russianCharacters = U8("дом");
string brazilianCharacters = U8("óculos");
EXPECT_EQ(httplib::encode_uri_component(chineseCharacters),
"%E4%B8%AD%E5%9B%BD%E8%AA%9E");
@@ -491,9 +506,9 @@ TEST(EncodeUriTest, ParseReservedCharactersTest) {
}
TEST(EncodeUriTest, TestUTF8Characters) {
string chineseCharacters = u8"中国語";
string russianCharacters = u8"дом";
string brazilianCharacters = u8"óculos";
string chineseCharacters = U8("中国語");
string russianCharacters = U8("дом");
string brazilianCharacters = U8("óculos");
EXPECT_EQ(httplib::encode_uri(chineseCharacters),
"%E4%B8%AD%E5%9B%BD%E8%AA%9E");
@@ -538,9 +553,9 @@ TEST(DecodeUriComponentTest, TestUTF8Characters) {
string encodedRussian = "%D0%B4%D0%BE%D0%BC";
string encodedBrazilian = "%C3%B3culos";
EXPECT_EQ(httplib::decode_uri_component(encodedChinese), u8"中国語");
EXPECT_EQ(httplib::decode_uri_component(encodedRussian), u8"дом");
EXPECT_EQ(httplib::decode_uri_component(encodedBrazilian), u8"óculos");
EXPECT_EQ(httplib::decode_uri_component(encodedChinese), U8("中国語"));
EXPECT_EQ(httplib::decode_uri_component(encodedRussian), U8("дом"));
EXPECT_EQ(httplib::decode_uri_component(encodedBrazilian), U8("óculos"));
}
TEST(DecodeUriComponentTest, TestPathComponentDecoding) {
@@ -567,9 +582,9 @@ TEST(DecodeUriTest, TestUTF8Characters) {
string encodedRussian = "%D0%B4%D0%BE%D0%BC";
string encodedBrazilian = "%C3%B3culos";
EXPECT_EQ(httplib::decode_uri(encodedChinese), u8"中国語");
EXPECT_EQ(httplib::decode_uri(encodedRussian), u8"дом");
EXPECT_EQ(httplib::decode_uri(encodedBrazilian), u8"óculos");
EXPECT_EQ(httplib::decode_uri(encodedChinese), U8("中国語"));
EXPECT_EQ(httplib::decode_uri(encodedRussian), U8("дом"));
EXPECT_EQ(httplib::decode_uri(encodedBrazilian), U8("óculos"));
}
TEST(DecodeUriTest, TestCompleteUri) {
@@ -3135,16 +3150,20 @@ protected:
[&](const Request &req, Response &res) {
ASSERT_FALSE(req.has_header("REMOTE_ADDR"));
ASSERT_FALSE(req.has_header("REMOTE_PORT"));
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
ASSERT_ANY_THROW(req.get_header_value("REMOTE_ADDR"));
ASSERT_ANY_THROW(req.get_header_value("REMOTE_PORT"));
#endif
res.set_content(req.remote_addr, "text/plain");
})
.Get("/local_addr",
[&](const Request &req, Response &res) {
ASSERT_FALSE(req.has_header("LOCAL_ADDR"));
ASSERT_FALSE(req.has_header("LOCAL_PORT"));
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
ASSERT_ANY_THROW(req.get_header_value("LOCAL_ADDR"));
ASSERT_ANY_THROW(req.get_header_value("LOCAL_PORT"));
#endif
auto local_addr = req.local_addr;
auto local_port = std::to_string(req.local_port);
res.set_content(local_addr.append(":").append(local_port),
@@ -6558,6 +6577,77 @@ TEST_F(ServerTest, PreCompressionLoggingOnlyPreLogger) {
EXPECT_EQ(test_content, pre_compression_body);
}
TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) {
{
// Test with Expect: 100-continue header - success case
// Server returns 100 Continue, client sends body, server returns 200 OK
Request req;
req.method = "POST";
req.path = "/post-large";
req.set_header("Expect", "100-continue");
req.body = LARGE_DATA;
Response res;
auto error = Error::Success;
cli_.set_keep_alive(true);
auto ret = cli_.send(req, res, error);
EXPECT_TRUE(ret);
EXPECT_EQ(StatusCode::OK_200, res.status);
EXPECT_EQ(LARGE_DATA, res.body);
}
{
// Test with Expect: 100-continue header - error case
// Client should not send the body when server returns an error
Request req;
req.method = "POST";
req.path = "/post-large?q=" + LONG_QUERY_VALUE;
req.set_header("Expect", "100-continue");
req.body = LARGE_DATA;
Response res;
auto error = Error::Success;
auto start = std::chrono::high_resolution_clock::now();
cli_.set_keep_alive(true);
auto ret = cli_.send(req, res, error);
auto end = std::chrono::high_resolution_clock::now();
auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
.count();
// With Expect: 100-continue, request completes successfully but with error
EXPECT_TRUE(ret);
EXPECT_EQ(StatusCode::UriTooLong_414, res.status);
EXPECT_EQ("close", res.get_header_value("Connection"));
EXPECT_FALSE(cli_.is_socket_open());
EXPECT_LE(elapsed, 200);
}
{
// Send an extra GET request to ensure error recovery without hanging
Request req;
req.method = "GET";
req.path = "/hi";
auto start = std::chrono::high_resolution_clock::now();
auto res = cli_.send(req);
auto end = std::chrono::high_resolution_clock::now();
auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
.count();
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ("Hello World!", res->body);
EXPECT_LE(elapsed, 100);
}
}
TEST(ZstdDecompressor, ChunkedDecompression) {
std::string data;
for (size_t i = 0; i < 32 * 1024; ++i) {
@@ -6974,6 +7064,7 @@ TEST(ServerStopTest, ListenFailure) {
t.join();
}
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
TEST(ServerStopTest, Decommision) {
Server svr;
@@ -7019,6 +7110,7 @@ TEST(ServerStopTest, Decommision) {
}
}
}
#endif
// Helper function for string body upload progress tests
template <typename SetupHandler, typename ClientCall>
@@ -7422,10 +7514,10 @@ TEST(MountTest, MultibytesPathName) {
Client cli("localhost", PORT);
auto res = cli.Get(u8"/日本語Dir/日本語File.txt");
auto res = cli.Get(U8("/日本語Dir/日本語File.txt"));
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(u8"日本語コンテンツ", res->body);
EXPECT_EQ(U8("日本語コンテンツ"), res->body);
}
TEST(KeepAliveTest, ReadTimeout) {
@@ -8637,6 +8729,31 @@ TEST(SSLClientTest, WildcardHostNameMatch_Online) {
ASSERT_EQ(StatusCode::OK_200, res->status);
}
TEST(SSLClientTest, WildcardHostNameMatchCase_Online) {
SSLClient cli("wWw.YouTube.Com");
cli.set_ca_cert_path(CA_CERT_FILE);
cli.enable_server_certificate_verification(true);
cli.enable_server_hostname_verification(true);
cli.set_follow_location(true);
auto res = cli.Get("/");
ASSERT_TRUE(res);
ASSERT_EQ(StatusCode::OK_200, res->status);
}
TEST(SSLClientTest, HostNameMatchCase_Online) {
SSLClient cli("gOoGlE.COm");
cli.enable_server_certificate_verification(true);
cli.enable_server_hostname_verification(true);
cli.set_follow_location(true);
auto res = cli.Get("/");
ASSERT_TRUE(res);
ASSERT_EQ(StatusCode::OK_200, res->status);
}
TEST(SSLClientTest, Issue2004_Online) {
Client client("https://google.com");
client.set_follow_location(true);
@@ -9602,6 +9719,27 @@ TEST(SSLClientRedirectTest, CertFile) {
ASSERT_EQ(StatusCode::OK_200, res->status);
}
// Test that set_ca_cert_store() skips system certs (consistent with
// set_ca_cert_path behavior). When a custom cert store is set, only those certs
// should be trusted - system certs should NOT be loaded.
TEST(SSLClientTest, SetCaCertStoreSkipsSystemCerts_Online) {
// Load a specific cert that is NOT a system CA cert
std::string cert;
read_file(SERVER_CERT2_FILE, cert);
SSLClient cli("google.com");
cli.load_ca_cert_store(cert.c_str(), cert.size());
cli.enable_server_certificate_verification(true);
// This should FAIL because:
// 1. We loaded only SERVER_CERT2 (a test cert, not a CA for google.com)
// 2. System certs should NOT be loaded when custom store is set
// If system certs WERE loaded, this would succeed
auto res = cli.Get("/");
ASSERT_FALSE(res);
EXPECT_EQ(Error::SSLServerVerification, res.error());
}
TEST(MultipartFormDataTest, LargeData) {
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
@@ -10461,7 +10599,11 @@ TEST(TaskQueueTest, IncreaseAtomicInteger) {
EXPECT_TRUE(queued);
}
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
task_queue->shutdown();
#else
EXPECT_NO_THROW(task_queue->shutdown());
#endif
EXPECT_EQ(number_of_tasks, count.load());
}
@@ -10480,7 +10622,11 @@ TEST(TaskQueueTest, IncreaseAtomicIntegerWithQueueLimit) {
}
}
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
task_queue->shutdown();
#else
EXPECT_NO_THROW(task_queue->shutdown());
#endif
EXPECT_EQ(queued_count, count.load());
EXPECT_TRUE(queued_count <= number_of_tasks);
EXPECT_TRUE(queued_count >= qlimit);
@@ -10546,7 +10692,11 @@ TEST(TaskQueueTest, MaxQueuedRequests) {
EXPECT_TRUE(queued);
}
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
task_queue->shutdown();
#else
EXPECT_NO_THROW(task_queue->shutdown());
#endif
}
TEST(RedirectTest, RedirectToUrlWithQueryParameters) {
@@ -10709,7 +10859,15 @@ TEST(VulnerabilityTest, CRLFInjectionInHeaders) {
}
std::string resp = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello";
::send(cli, resp.c_str(), resp.size(), 0);
::send(cli,
#ifdef _WIN32
static_cast<const char *>(resp.c_str()),
static_cast<int>(resp.size()),
#else
resp.c_str(), resp.size(),
#endif
0);
buf_all.erase(0, pos + 4);
}
@@ -11009,7 +11167,7 @@ TEST(MakeHostAndPortStringTest, VariousPatterns) {
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
TEST(SSLClientHostHeaderTest, Issue2301) {
TEST(SSLClientHostHeaderTest, Issue2301_Online) {
httplib::SSLClient cli("roblox.com", 443);
cli.set_follow_location(true);
@@ -13407,10 +13565,11 @@ protected:
msg.id = value;
} else if (field == "retry") {
// Parse retry interval in milliseconds
try {
retry_ms = std::stoi(value);
} catch (...) {
// Invalid retry value, ignore
{
int v = 0;
auto res =
detail::from_chars(value.data(), value.data() + value.size(), v);
if (res.ec == std::errc{}) { retry_ms = v; }
}
}
// Unknown fields are ignored per SSE spec