mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc6faf5c17 | |||
| 71bb17fb0e | |||
| 094bf112bb | |||
| fbec2a3466 | |||
| c3fa06112b | |||
| f73e694f0c | |||
| 191bfb2ea4 | |||
| ad5839f0d1 | |||
| 02dfb97fd6 | |||
| a38a076571 | |||
| 0e1b52b23e | |||
| c0469eba96 | |||
| 7dec57d1eb | |||
| b85aa76bd2 | |||
| cea018f2cd | |||
| 1111219f17 | |||
| a7e1d14b15 | |||
| 6eff49e1fb | |||
| bd95e67c23 | |||
| 2e2e47bab1 | |||
| 59905c7f0d | |||
| 8d03ef1615 | |||
| 23a1d79a66 | |||
| 781c55f120 | |||
| 40f7985e02 | |||
| f85f30a637 | |||
| 6da7f0c61c | |||
| 2ba0e7a797 | |||
| ded82448aa | |||
| 98048a033a | |||
| 7ae794a6bf | |||
| 385adefb11 | |||
| b7c2f04318 | |||
| d23cf77cd0 |
@@ -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"
|
||||
+7
-4
@@ -185,8 +185,11 @@ elseif(HTTPLIB_USE_BROTLI_IF_AVAILABLE)
|
||||
set(HTTPLIB_IS_USING_BROTLI ${Brotli_FOUND})
|
||||
endif()
|
||||
|
||||
# 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)
|
||||
@@ -194,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)
|
||||
@@ -276,9 +279,9 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
||||
$<$<PLATFORM_ID:Windows>:ws2_32>
|
||||
$<$<PLATFORM_ID:Windows>:crypt32>
|
||||
# Needed for API from MacOS Security framework
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN}>>:-framework CoreFoundation -framework Security>"
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
|
||||
# Needed for non-blocking getaddrinfo on MacOS
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork>"
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
||||
# Can't put multiple targets in a single generator expression or it bugs out.
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
|
||||
|
||||
@@ -1350,7 +1350,7 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
|
||||
License
|
||||
-------
|
||||
|
||||
MIT license (© 2025 Yuji Hirose)
|
||||
MIT license (© 2026 Yuji Hirose)
|
||||
|
||||
Special Thanks To
|
||||
-----------------
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// main.cc
|
||||
//
|
||||
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// client.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// hello.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// redirect.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// sample.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// server_and_client.cc
|
||||
//
|
||||
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// simplecli.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// simplesvr.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// ssecli-stream.cc
|
||||
//
|
||||
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
// SSE (Server-Sent Events) client example using Streaming API
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// ssecli.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// upload.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
//
|
||||
// httplib.h
|
||||
//
|
||||
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.29.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x001D00"
|
||||
#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
|
||||
@@ -205,7 +221,10 @@
|
||||
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
|
||||
#ifndef _SSIZE_T_DEFINED
|
||||
using ssize_t = __int64;
|
||||
#define _SSIZE_T_DEFINED
|
||||
#endif
|
||||
#endif // _MSC_VER
|
||||
|
||||
#ifndef S_ISREG
|
||||
@@ -283,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>
|
||||
@@ -302,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>
|
||||
@@ -491,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 {
|
||||
@@ -1845,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);
|
||||
@@ -2443,16 +2529,20 @@ namespace detail {
|
||||
|
||||
#if defined(_WIN32)
|
||||
inline std::wstring u8string_to_wstring(const char *s) {
|
||||
std::wstring ws;
|
||||
if (!s) { return std::wstring(); }
|
||||
|
||||
auto len = static_cast<int>(strlen(s));
|
||||
if (!len) { return std::wstring(); }
|
||||
|
||||
auto wlen = ::MultiByteToWideChar(CP_UTF8, 0, s, len, nullptr, 0);
|
||||
if (wlen > 0) {
|
||||
ws.resize(wlen);
|
||||
wlen = ::MultiByteToWideChar(
|
||||
CP_UTF8, 0, s, len,
|
||||
const_cast<LPWSTR>(reinterpret_cast<LPCWSTR>(ws.data())), wlen);
|
||||
if (wlen != static_cast<int>(ws.size())) { ws.clear(); }
|
||||
}
|
||||
if (!wlen) { return std::wstring(); }
|
||||
|
||||
std::wstring ws;
|
||||
ws.resize(wlen);
|
||||
wlen = ::MultiByteToWideChar(
|
||||
CP_UTF8, 0, s, len,
|
||||
const_cast<LPWSTR>(reinterpret_cast<LPCWSTR>(ws.data())), wlen);
|
||||
if (wlen != static_cast<int>(ws.size())) { ws.clear(); }
|
||||
return ws;
|
||||
}
|
||||
#endif
|
||||
@@ -3236,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
|
||||
@@ -3522,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
|
||||
@@ -4543,6 +4636,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
|
||||
return ret;
|
||||
#elif TARGET_OS_MAC
|
||||
if (!node) { return EAI_NONAME; }
|
||||
// macOS implementation using CFHost API for asynchronous DNS resolution
|
||||
CFStringRef hostname_ref = CFStringCreateWithCString(
|
||||
kCFAllocatorDefault, node, kCFStringEncodingUTF8);
|
||||
@@ -4780,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;
|
||||
|
||||
@@ -6300,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;
|
||||
@@ -6378,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;
|
||||
@@ -8457,6 +8553,23 @@ make_host_and_port_string_always_port(const std::string &host, int port) {
|
||||
return prepare_host_string(host) + ":" + std::to_string(port);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool check_and_write_headers(Stream &strm, Headers &headers,
|
||||
T header_writer, Error &error) {
|
||||
for (const auto &h : headers) {
|
||||
if (!detail::fields::is_field_name(h.first) ||
|
||||
!detail::fields::is_field_value(h.second)) {
|
||||
error = Error::InvalidHeaders;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (header_writer(strm, headers) <= 0) {
|
||||
error = Error::Write;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// HTTP server implementation
|
||||
@@ -8864,7 +8977,7 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
||||
{
|
||||
detail::BufferStream bstrm;
|
||||
if (!detail::write_response_line(bstrm, res.status)) { return false; }
|
||||
if (!header_writer_(bstrm, res.headers)) { return false; }
|
||||
if (header_writer_(bstrm, res.headers) <= 0) { return false; }
|
||||
|
||||
// Flush buffer
|
||||
auto &data = bstrm.get_buffer();
|
||||
@@ -8957,7 +9070,26 @@ inline bool Server::read_content(Stream &strm, Request &req, Response &res) {
|
||||
strm, req, res,
|
||||
// Regular
|
||||
[&](const char *buf, size_t n) {
|
||||
if (req.body.size() + n > req.body.max_size()) { return false; }
|
||||
// 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 (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;
|
||||
},
|
||||
@@ -9047,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;
|
||||
@@ -9637,6 +9776,10 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
|
||||
Request req;
|
||||
req.start_time_ = std::chrono::steady_clock::now();
|
||||
req.remote_addr = remote_addr;
|
||||
req.remote_port = remote_port;
|
||||
req.local_addr = local_addr;
|
||||
req.local_port = local_port;
|
||||
|
||||
Response res;
|
||||
res.version = "HTTP/1.1";
|
||||
@@ -10033,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());
|
||||
@@ -10054,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
|
||||
|
||||
@@ -10346,8 +10490,8 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
||||
return handle;
|
||||
}
|
||||
|
||||
if (!detail::write_headers(strm, req.headers)) {
|
||||
handle.error = Error::Write;
|
||||
if (!detail::check_and_write_headers(strm, req.headers, header_writer_,
|
||||
handle.error)) {
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
@@ -10840,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")) {
|
||||
@@ -10944,7 +11089,11 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
|
||||
// Write request line and headers
|
||||
detail::write_request_line(bstrm, req.method, path_with_query);
|
||||
header_writer_(bstrm, req.headers);
|
||||
if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
|
||||
error)) {
|
||||
output_error_log(error, &req);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Flush buffer
|
||||
auto &data = bstrm.get_buffer();
|
||||
@@ -10955,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);
|
||||
}
|
||||
@@ -11131,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()) {
|
||||
@@ -11147,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") {
|
||||
@@ -12916,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 =
|
||||
@@ -13163,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
|
||||
@@ -13178,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;
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ if openssl_dep.found()
|
||||
deps += openssl_dep
|
||||
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
|
||||
if host_machine.system() == 'darwin'
|
||||
macosx_keychain_dep = dependency('appleframeworks', modules: ['CoreFoundation', 'Security'], required: get_option('macosx_keychain'))
|
||||
macosx_keychain_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation', 'Security'], required: get_option('macosx_keychain'))
|
||||
if macosx_keychain_dep.found()
|
||||
deps += macosx_keychain_dep
|
||||
args += '-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN'
|
||||
@@ -85,7 +85,7 @@ async_ns_opt = get_option('non_blocking_getaddrinfo')
|
||||
if host_machine.system() == 'windows'
|
||||
async_ns_dep = cxx.find_library('ws2_32', required: async_ns_opt)
|
||||
elif host_machine.system() == 'darwin'
|
||||
async_ns_dep = dependency('appleframeworks', modules: ['CFNetwork'], required: async_ns_opt)
|
||||
async_ns_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation'], required: async_ns_opt)
|
||||
else
|
||||
async_ns_dep = cxx.find_library('anl', required: async_ns_opt)
|
||||
endif
|
||||
|
||||
+394
-78
@@ -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);
|
||||
@@ -82,15 +97,13 @@ static void read_file(const std::string &path, std::string &out) {
|
||||
}
|
||||
|
||||
void performance_test(const char *host) {
|
||||
auto port = 1234;
|
||||
|
||||
Server svr;
|
||||
|
||||
svr.Get("/benchmark", [&](const Request & /*req*/, Response &res) {
|
||||
res.set_content("Benchmark Response", "text/plain");
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&]() { svr.listen(host, port); });
|
||||
auto listen_thread = std::thread([&]() { svr.listen(host, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
@@ -99,7 +112,7 @@ void performance_test(const char *host) {
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli(host, port);
|
||||
Client cli(host, PORT);
|
||||
|
||||
// Warm-up request to establish connection and resolve DNS
|
||||
auto warmup_res = cli.Get("/benchmark");
|
||||
@@ -374,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) {
|
||||
@@ -432,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");
|
||||
@@ -459,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");
|
||||
@@ -493,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");
|
||||
@@ -540,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) {
|
||||
@@ -569,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) {
|
||||
@@ -2235,7 +2248,7 @@ TEST(ReceiveSignals, Signal) {
|
||||
int port = 0;
|
||||
auto thread = std::thread([&]() {
|
||||
setupSignalHandlers();
|
||||
port = svr.bind_to_any_port("localhost");
|
||||
port = svr.bind_to_any_port(HOST);
|
||||
svr.listen_after_bind();
|
||||
});
|
||||
auto se = detail::scope_exit([&] {
|
||||
@@ -2261,7 +2274,7 @@ TEST(RedirectToDifferentPort, Redirect) {
|
||||
|
||||
int svr1_port = 0;
|
||||
auto thread1 = std::thread([&]() {
|
||||
svr1_port = svr1.bind_to_any_port("localhost");
|
||||
svr1_port = svr1.bind_to_any_port(HOST);
|
||||
svr1.listen_after_bind();
|
||||
});
|
||||
|
||||
@@ -2272,7 +2285,7 @@ TEST(RedirectToDifferentPort, Redirect) {
|
||||
|
||||
int svr2_port = 0;
|
||||
auto thread2 = std::thread([&]() {
|
||||
svr2_port = svr2.bind_to_any_port("localhost");
|
||||
svr2_port = svr2.bind_to_any_port(HOST);
|
||||
svr2.listen_after_bind();
|
||||
});
|
||||
auto se = detail::scope_exit([&] {
|
||||
@@ -3137,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),
|
||||
@@ -6560,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) {
|
||||
@@ -6845,6 +6933,29 @@ TEST(ServerRequestParsingTest, InvalidSpaceInURL) {
|
||||
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24));
|
||||
}
|
||||
|
||||
TEST(ServerRequestParsingTest, RemoteAddrSetOnBadRequest) {
|
||||
Server svr;
|
||||
|
||||
svr.set_error_handler([&](const Request &req, Response & /*res*/) {
|
||||
EXPECT_TRUE(!req.remote_addr.empty());
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
// Send an invalid request line to trigger Bad Request
|
||||
const std::string bad_req = "BADMETHOD / HTTP/1.1\r\nHost: localhost\r\n\r\n";
|
||||
std::string out;
|
||||
ASSERT_TRUE(send_request(5, bad_req, &out));
|
||||
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24));
|
||||
}
|
||||
|
||||
TEST(ServerRequestParsingTest, InvalidFieldValueContains_CR_LF_NUL) {
|
||||
std::string out;
|
||||
std::string request(
|
||||
@@ -6953,6 +7064,7 @@ TEST(ServerStopTest, ListenFailure) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
TEST(ServerStopTest, Decommision) {
|
||||
Server svr;
|
||||
|
||||
@@ -6998,6 +7110,7 @@ TEST(ServerStopTest, Decommision) {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Helper function for string body upload progress tests
|
||||
template <typename SetupHandler, typename ClientCall>
|
||||
@@ -7401,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) {
|
||||
@@ -8616,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);
|
||||
@@ -9581,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);
|
||||
|
||||
@@ -9616,7 +9775,7 @@ TEST(MultipartFormDataTest, LargeData) {
|
||||
}
|
||||
});
|
||||
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8080); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, 8080); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
@@ -10440,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());
|
||||
}
|
||||
|
||||
@@ -10459,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);
|
||||
@@ -10525,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) {
|
||||
@@ -10650,6 +10821,77 @@ TEST(VulnerabilityTest, CRLFInjection) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VulnerabilityTest, CRLFInjectionInHeaders) {
|
||||
auto server_thread = std::thread([] {
|
||||
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
default_socket_options(srv);
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 1);
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
|
||||
::listen(srv, 1);
|
||||
|
||||
sockaddr_in cli_addr{};
|
||||
socklen_t cli_len = sizeof(cli_addr);
|
||||
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
|
||||
|
||||
detail::set_socket_opt_time(cli, SOL_SOCKET, SO_RCVTIMEO, 1, 0);
|
||||
|
||||
std::string buf_all;
|
||||
char buf[2048];
|
||||
ssize_t n;
|
||||
|
||||
while ((n = ::recv(cli, buf, sizeof(buf), 0)) > 0) {
|
||||
buf_all.append(buf, static_cast<size_t>(n));
|
||||
|
||||
size_t pos;
|
||||
while ((pos = buf_all.find("\r\n\r\n")) != std::string::npos) {
|
||||
auto request_block = buf_all.substr(0, pos + 4); // include separator
|
||||
|
||||
auto e = request_block.find("\r\n");
|
||||
if (e != std::string::npos) {
|
||||
auto request_line = request_block.substr(0, e);
|
||||
std::string msg =
|
||||
"CRLF injection detected in request line: '" + request_line + "'";
|
||||
EXPECT_FALSE(true) << msg;
|
||||
}
|
||||
|
||||
std::string resp = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello";
|
||||
|
||||
::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);
|
||||
}
|
||||
}
|
||||
|
||||
detail::close_socket(cli);
|
||||
detail::close_socket(srv);
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
auto cli = Client("127.0.0.1", PORT + 1);
|
||||
|
||||
auto headers = Headers{
|
||||
{"A", "B\r\n\r\nGET /pwned HTTP/1.1\r\nHost: 127.0.0.1:1234\r\n\r\n"},
|
||||
{"Connection", "keep-alive"}};
|
||||
|
||||
auto res = cli.Get("/hi", headers);
|
||||
EXPECT_FALSE(res);
|
||||
EXPECT_EQ(Error::InvalidHeaders, res.error());
|
||||
|
||||
server_thread.join();
|
||||
}
|
||||
|
||||
TEST(PathParamsTest, StaticMatch) {
|
||||
const auto pattern = "/users/all";
|
||||
detail::PathParamsMatcher matcher(pattern);
|
||||
@@ -10925,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);
|
||||
|
||||
@@ -11679,12 +11921,16 @@ TEST(ForwardedHeadersTest, HandlesWhitespaceAroundIPs) {
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli(HOST, PORT);
|
||||
auto res = cli.Get("/ip", {{"X-Forwarded-For",
|
||||
" 198.51.100.23 , 203.0.113.66 , 192.0.2.45 "}});
|
||||
std::string raw_req =
|
||||
"GET /ip HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"X-Forwarded-For: 198.51.100.23 , 203.0.113.66 , 192.0.2.45 \r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
std::string out;
|
||||
ASSERT_TRUE(send_request(5, raw_req, &out));
|
||||
EXPECT_EQ("HTTP/1.1 200 OK", out.substr(0, 15));
|
||||
|
||||
// Header parser trims surrounding whitespace of the header value
|
||||
EXPECT_EQ(observed_xff, "198.51.100.23 , 203.0.113.66 , 192.0.2.45");
|
||||
@@ -12247,7 +12493,7 @@ static void test_compression_parity(const std::string &original,
|
||||
res.set_header("Content-Encoding", encoding);
|
||||
});
|
||||
|
||||
auto t = std::thread([&] { svr.listen("localhost", 1234); });
|
||||
auto t = std::thread([&] { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
@@ -12256,7 +12502,7 @@ static void test_compression_parity(const std::string &original,
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 1234);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
// Non-streaming
|
||||
{
|
||||
@@ -12373,7 +12619,7 @@ protected:
|
||||
[](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_header("Allow", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
});
|
||||
thread_ = std::thread([this]() { svr_.listen("localhost", 8790); });
|
||||
thread_ = std::thread([this]() { svr_.listen(HOST, PORT); });
|
||||
svr_.wait_until_ready();
|
||||
}
|
||||
void TearDown() override {
|
||||
@@ -12386,7 +12632,7 @@ protected:
|
||||
|
||||
// stream::Get tests
|
||||
TEST_F(StreamApiTest, GetBasic) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
auto result = httplib::stream::Get(cli, "/hello");
|
||||
ASSERT_TRUE(result.is_valid());
|
||||
EXPECT_EQ(200, result.status());
|
||||
@@ -12394,7 +12640,7 @@ TEST_F(StreamApiTest, GetBasic) {
|
||||
}
|
||||
|
||||
TEST_F(StreamApiTest, GetWithParams) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
httplib::Params params{{"foo", "bar"}};
|
||||
auto result = httplib::stream::Get(cli, "/echo-params", params);
|
||||
ASSERT_TRUE(result.is_valid());
|
||||
@@ -12402,12 +12648,12 @@ TEST_F(StreamApiTest, GetWithParams) {
|
||||
}
|
||||
|
||||
TEST_F(StreamApiTest, GetConnectionError) {
|
||||
httplib::Client cli("localhost", 9999);
|
||||
httplib::Client cli(HOST, 9999);
|
||||
EXPECT_FALSE(httplib::stream::Get(cli, "/hello").is_valid());
|
||||
}
|
||||
|
||||
TEST_F(StreamApiTest, Get404) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
auto result = httplib::stream::Get(cli, "/nonexistent");
|
||||
EXPECT_TRUE(result.is_valid());
|
||||
EXPECT_EQ(404, result.status());
|
||||
@@ -12415,7 +12661,7 @@ TEST_F(StreamApiTest, Get404) {
|
||||
|
||||
// stream::Post tests
|
||||
TEST_F(StreamApiTest, PostBasic) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
auto result = httplib::stream::Post(cli, "/echo", R"({"key":"value"})",
|
||||
"application/json");
|
||||
ASSERT_TRUE(result.is_valid());
|
||||
@@ -12424,7 +12670,7 @@ TEST_F(StreamApiTest, PostBasic) {
|
||||
}
|
||||
|
||||
TEST_F(StreamApiTest, PostWithHeaders) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
httplib::Headers headers{{"X-Custom", "value"}};
|
||||
auto result = httplib::stream::Post(cli, "/echo-headers", headers, "body",
|
||||
"text/plain");
|
||||
@@ -12432,7 +12678,7 @@ TEST_F(StreamApiTest, PostWithHeaders) {
|
||||
}
|
||||
|
||||
TEST_F(StreamApiTest, PostWithParams) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
httplib::Params params{{"k", "v"}};
|
||||
auto result =
|
||||
httplib::stream::Post(cli, "/echo-params", params, "data", "text/plain");
|
||||
@@ -12442,7 +12688,7 @@ TEST_F(StreamApiTest, PostWithParams) {
|
||||
}
|
||||
|
||||
TEST_F(StreamApiTest, PostLarge) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
auto result = httplib::stream::Post(cli, "/large", "", "text/plain");
|
||||
size_t total = 0;
|
||||
while (result.next()) {
|
||||
@@ -12453,7 +12699,7 @@ TEST_F(StreamApiTest, PostLarge) {
|
||||
|
||||
// stream::Put/Patch tests
|
||||
TEST_F(StreamApiTest, PutAndPatch) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
auto put = httplib::stream::Put(cli, "/echo", "test", "text/plain");
|
||||
EXPECT_EQ("PUT:test", read_body(put));
|
||||
auto patch = httplib::stream::Patch(cli, "/echo", "test", "text/plain");
|
||||
@@ -12462,7 +12708,7 @@ TEST_F(StreamApiTest, PutAndPatch) {
|
||||
|
||||
// stream::Delete tests
|
||||
TEST_F(StreamApiTest, Delete) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
auto del1 = httplib::stream::Delete(cli, "/resource");
|
||||
EXPECT_EQ("Deleted", read_body(del1));
|
||||
auto del2 = httplib::stream::Delete(cli, "/resource", "data", "text/plain");
|
||||
@@ -12471,7 +12717,7 @@ TEST_F(StreamApiTest, Delete) {
|
||||
|
||||
// stream::Head/Options tests
|
||||
TEST_F(StreamApiTest, HeadAndOptions) {
|
||||
httplib::Client cli("localhost", 8790);
|
||||
httplib::Client cli(HOST, PORT);
|
||||
auto head = httplib::stream::Head(cli, "/head-test");
|
||||
EXPECT_TRUE(head.is_valid());
|
||||
EXPECT_FALSE(head.get_header_value("Content-Length").empty());
|
||||
@@ -12749,10 +12995,10 @@ TEST(ETagTest, StaticFileETagAndIfNoneMatch) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8087); });
|
||||
auto t = std::thread([&]() { svr.listen("localhost", PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8087);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
// First request: should get 200 with ETag header
|
||||
auto res1 = cli.Get("/static/etag_testfile.txt");
|
||||
@@ -12803,10 +13049,10 @@ TEST(ETagTest, StaticFileETagIfNoneMatchStarNotFound) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8090); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8090);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
// Send If-None-Match: * to a non-existent file
|
||||
Headers h = {{"If-None-Match", "*"}};
|
||||
@@ -12832,10 +13078,10 @@ TEST(ETagTest, LastModifiedAndIfModifiedSince) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8088); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8088);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
// First request: should get 200 with Last-Modified header
|
||||
auto res1 = cli.Get("/static/ims_testfile.txt");
|
||||
@@ -12890,10 +13136,10 @@ TEST(ETagTest, VaryAcceptEncodingWithCompression) {
|
||||
res.set_content(body, "text/plain");
|
||||
});
|
||||
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8089); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8089);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
// Request with gzip support: should get Vary header when compressed
|
||||
cli.set_compress(true);
|
||||
@@ -12934,10 +13180,10 @@ TEST(ETagTest, IfRangeWithETag) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8090); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8090);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
// First request: get ETag
|
||||
auto res1 = cli.Get("/static/if_range_testfile.txt");
|
||||
@@ -12995,10 +13241,10 @@ TEST(ETagTest, IfRangeWithDate) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8091); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8091);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
// First request: get Last-Modified
|
||||
auto res1 = cli.Get("/static/if_range_date_testfile.txt");
|
||||
@@ -13047,10 +13293,10 @@ TEST(ETagTest, MalformedIfNoneMatchAndWhitespace) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8092); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8092);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
// baseline: should get 200 and an ETag
|
||||
auto res1 = cli.Get("/static/etag_malformed.txt");
|
||||
@@ -13065,10 +13311,15 @@ TEST(ETagTest, MalformedIfNoneMatchAndWhitespace) {
|
||||
EXPECT_EQ(200, res_bad->status);
|
||||
|
||||
// Whitespace-only header value should be considered invalid / non-matching
|
||||
Headers h_space = {{"If-None-Match", " "}};
|
||||
auto res_space = cli.Get("/static/etag_malformed.txt", h_space);
|
||||
ASSERT_TRUE(res_space);
|
||||
EXPECT_EQ(200, res_space->status);
|
||||
std::string raw_req = "GET /static/etag_malformed.txt HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"If-None-Match: \r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
std::string out;
|
||||
ASSERT_TRUE(send_request(5, raw_req, &out));
|
||||
EXPECT_EQ("HTTP/1.1 200 OK", out.substr(0, 15));
|
||||
|
||||
svr.stop();
|
||||
t.join();
|
||||
@@ -13088,10 +13339,10 @@ TEST(ETagTest, InvalidIfModifiedSinceAndIfRangeDate) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8093); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8093);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
auto res1 = cli.Get("/static/ims_invalid_format.txt");
|
||||
ASSERT_TRUE(res1);
|
||||
@@ -13130,10 +13381,10 @@ TEST(ETagTest, IfRangeWithMalformedETag) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8094); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8094);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
// First request: get ETag
|
||||
auto res1 = cli.Get("/static/ifrange_malformed.txt");
|
||||
@@ -13167,10 +13418,10 @@ TEST(ETagTest, ExtremeLargeDateValues) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8095); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8095);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
auto res1 = cli.Get(std::string("/static/") + fname);
|
||||
ASSERT_TRUE(res1);
|
||||
@@ -13230,10 +13481,10 @@ TEST(ETagTest, NegativeFileModificationTime) {
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8096); });
|
||||
auto t = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8096);
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
auto res1 = cli.Get(std::string("/static/") + fname);
|
||||
ASSERT_TRUE(res1);
|
||||
@@ -13314,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
|
||||
@@ -13559,7 +13811,7 @@ protected:
|
||||
}
|
||||
|
||||
void start_server() {
|
||||
port_ = server_->bind_to_any_port("localhost");
|
||||
port_ = server_->bind_to_any_port(HOST);
|
||||
server_thread_ = std::thread([this]() { server_->listen_after_bind(); });
|
||||
|
||||
// Wait for server to start
|
||||
@@ -13993,3 +14245,67 @@ TEST_F(SSEIntegrationTest, LastEventIdSentOnReconnect) {
|
||||
EXPECT_EQ(received_last_event_ids[1], "event-0");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Issue2318Test, EmptyHostString) {
|
||||
{
|
||||
httplib::Client cli_empty("", PORT);
|
||||
auto res = cli_empty.Get("/");
|
||||
ASSERT_FALSE(res);
|
||||
EXPECT_EQ(httplib::Error::Connection, res.error());
|
||||
}
|
||||
{
|
||||
httplib::Client cli(" ", PORT);
|
||||
auto res = cli.Get("/");
|
||||
ASSERT_FALSE(res);
|
||||
EXPECT_EQ(httplib::Error::Connection, res.error());
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
TEST(ZipBombProtectionTest, DecompressedSizeExceedsLimit) {
|
||||
Server svr;
|
||||
|
||||
// Set a small payload limit (1KB)
|
||||
svr.set_payload_max_length(1024);
|
||||
|
||||
svr.Post("/test", [&](const Request &req, Response &res) {
|
||||
res.set_content("Body size: " + std::to_string(req.body.size()),
|
||||
"text/plain");
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
// Create data that compresses well but exceeds limit when decompressed
|
||||
// 8KB of repeated null bytes compresses to a very small size
|
||||
std::string original_data(8 * 1024, '\0');
|
||||
|
||||
// Compress the data using gzip
|
||||
std::string compressed_data;
|
||||
detail::gzip_compressor compressor;
|
||||
compressor.compress(original_data.data(), original_data.size(), true,
|
||||
[&](const char *data, size_t size) {
|
||||
compressed_data.append(data, size);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Verify compression worked (compressed should be much smaller)
|
||||
ASSERT_LT(compressed_data.size(), original_data.size());
|
||||
ASSERT_LT(compressed_data.size(), 1024u); // Compressed fits in limit
|
||||
|
||||
// Send compressed data with Content-Encoding: gzip
|
||||
Client cli(HOST, PORT);
|
||||
Headers headers = {{"Content-Encoding", "gzip"}};
|
||||
auto res =
|
||||
cli.Post("/test", headers, compressed_data, "application/octet-stream");
|
||||
|
||||
// Server should reject because decompressed size (8KB) exceeds limit (1KB)
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user