mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd95e67c23 | |||
| 2e2e47bab1 | |||
| 59905c7f0d | |||
| 8d03ef1615 | |||
| 23a1d79a66 | |||
| 781c55f120 | |||
| 40f7985e02 | |||
| f85f30a637 | |||
| 6da7f0c61c | |||
| 2ba0e7a797 | |||
| ded82448aa | |||
| 98048a033a | |||
| 7ae794a6bf | |||
| 385adefb11 | |||
| b7c2f04318 | |||
| d23cf77cd0 |
+4
-2
@@ -185,6 +185,8 @@ 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.
|
||||
if(HTTPLIB_REQUIRE_ZSTD)
|
||||
find_package(zstd)
|
||||
if(NOT zstd_FOUND)
|
||||
@@ -276,9 +278,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.1"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x001E01"
|
||||
|
||||
/*
|
||||
* Platform compatibility check
|
||||
@@ -205,7 +205,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
|
||||
@@ -2443,16 +2446,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
|
||||
@@ -4543,6 +4550,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);
|
||||
@@ -8457,6 +8465,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 +8889,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 +8982,13 @@ 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; }
|
||||
// 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()) {
|
||||
return false;
|
||||
}
|
||||
req.body.append(buf, n);
|
||||
return true;
|
||||
},
|
||||
@@ -9637,6 +9668,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";
|
||||
@@ -10346,8 +10381,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;
|
||||
}
|
||||
@@ -10944,7 +10979,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();
|
||||
|
||||
+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
|
||||
|
||||
+211
-54
@@ -82,15 +82,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 +97,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");
|
||||
@@ -2235,7 +2233,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 +2259,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 +2270,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([&] {
|
||||
@@ -6845,6 +6843,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(
|
||||
@@ -9616,7 +9637,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();
|
||||
@@ -10650,6 +10671,69 @@ 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, resp.c_str(), resp.size(), 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);
|
||||
@@ -11679,12 +11763,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 +12335,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 +12344,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 +12461,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 +12474,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 +12482,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 +12490,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 +12503,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 +12512,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 +12520,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 +12530,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 +12541,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 +12550,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 +12559,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 +12837,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 +12891,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 +12920,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 +12978,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 +13022,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 +13083,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 +13135,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 +13153,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 +13181,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 +13223,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 +13260,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 +13323,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);
|
||||
@@ -13559,7 +13652,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 +14086,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