mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc6faf5c17 | |||
| 71bb17fb0e | |||
| 094bf112bb | |||
| fbec2a3466 | |||
| c3fa06112b | |||
| f73e694f0c | |||
| 191bfb2ea4 | |||
| ad5839f0d1 | |||
| 02dfb97fd6 | |||
| a38a076571 | |||
| 0e1b52b23e | |||
| c0469eba96 | |||
| 7dec57d1eb | |||
| b85aa76bd2 | |||
| cea018f2cd | |||
| 1111219f17 | |||
| a7e1d14b15 | |||
| 6eff49e1fb |
@@ -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"
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user