Compare commits

...

19 Commits

Author SHA1 Message Date
yhirose eacc1ca98e Release v0.27.0 2025-10-27 19:57:53 -04:00
yhirose ac9ebb0ee3 Merge commit from fork
* Fix "Untrusted HTTP Header Handling (REMOTE*/LOCAL*)"

* Fix "Untrusted HTTP Header Handling (X-Forwarded-For)"

* Fix security problems in docker/main.cc
2025-10-27 19:54:12 -04:00
yhirose 11eed05ce7 Fix #2255 and #2256 2025-10-27 19:51:55 -04:00
yhirose f3bba0646a Fix benchmark test issue on Windows (#2258) 2025-10-27 18:27:55 -04:00
yhirose 2da189f88c Fix EventDispatcher problem (#2257) 2025-10-27 18:10:52 -04:00
yhirose 6e0f211cff Fix problem with .gitignore for examples 2025-10-27 17:54:27 -04:00
yhirose 318a3fe425 Fix problem with installing OpenSSL for Windows (#2254) 2025-10-25 22:35:21 -04:00
yhirose 2d8d524178 Fix #2251 2025-10-25 22:06:42 -04:00
yhirose afa88dbe70 Fix #2250 2025-10-25 21:36:53 -04:00
yhirose 08133b593b Merge branch 'staticlibs-ssl_error_reporting' 2025-10-25 19:32:31 -04:00
yhirose 8aedbf4547 Add a unit test 2025-10-25 19:31:48 -04:00
yhirose cde29362ef Merge branch 'ssl_error_reporting' of github.com:staticlibs/cpp-httplib into staticlibs-ssl_error_reporting 2025-10-25 19:09:09 -04:00
yhirose bae40fcdf2 Resolve #2237 2025-10-25 16:48:45 -04:00
crueter db561f5552 [cmake] FindBrotli: do not add Brotli:: targets if they already exist (#2249)
Not checking for this is terrible practice.
2025-10-16 09:59:15 -04:00
Andrea Pappacoda 35c52c1ab9 build(meson): use C++17 for gtest >= 1.17.0 (#2241) 2025-09-20 15:06:49 -04:00
Alex Kasko 23ff9a5605 Fix error reporting in SSLClient
When the `SSLClient` is used to connect to a plain-HTTP server (which
can happen in clients due to some end-user misconfiguration) it can
return a failure from the `send()` call without setting the `Error`
reference to the corresponding error code. This can cause problems to
callers, that may expect that, when the check like this is passed on
the response:

```c++
if (res.error() == Error::Success)
```

then they can access the response contents with `res.value()`. When
`SSLClient`'s connection fails - the contents `unique_ptr` is not set
and an attemt to access it causes UB.

This change fixes the `SSLClient::create_and_connect_socket` method
making sure that, the `Error` value is set correctly when the
`is_valid()` check fails.
2025-09-18 13:44:39 +01:00
yhirose 41be1e24e3 Code cleanup 2025-09-15 07:59:53 -04:00
Jonas van den Berg 6e52d0a057 Fix UB by use of dangling references in getaddrinfo_with_timeout (#2232)
* Fix use of dangling references

When the resolve thread is detached, local variables were still used, which could lead to a program crash.

* Add test to verify dangling ref fix

* Add missing brace initialization

* Assert that the remaining fields are really zeroed

* Fix use of chrono literals
2025-09-14 20:05:09 -04:00
apocelipes f72b4582e6 Fix: Fix Windows Cross-Compilation (#2234) 2025-09-14 08:05:51 -04:00
9 changed files with 1009 additions and 168 deletions
+1 -1
View File
@@ -148,7 +148,7 @@ jobs:
run: vcpkg install gtest curl zlib brotli zstd
- name: Install OpenSSL
if: ${{ matrix.config.with_ssl }}
run: choco install openssl --version 3.5.2 # workaround for chocolatey issue with the latest OpenSSL
run: choco install openssl
- name: Configure CMake ${{ matrix.config.name }}
run: >
cmake -B build -S .
+13 -1
View File
@@ -1,16 +1,28 @@
tags
# Ignore executables (no extension) but not source files
example/server
!example/server.*
example/client
!example/client.*
example/hello
!example/hello.*
example/simplecli
!example/simplecli.*
example/simplesvr
!example/simplesvr.*
example/benchmark
!example/benchmark.*
example/redirect
example/sse*
!example/redirect.*
example/ssecli
example/ssesvr
example/upload
!example/upload.*
example/one_time_request
!example/one_time_request.*
example/server_and_client
!example/server_and_client.*
example/*.pem
test/httplib.cc
test/httplib.h
+9 -2
View File
@@ -117,8 +117,15 @@ if(BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
if(CMAKE_SYSTEM_NAME MATCHES "Windows" AND ${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0")
message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.")
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
if(CMAKE_SYSTEM_VERSION)
if(${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0")
message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.")
endif()
else()
set(CMAKE_SYSTEM_VERSION "10.0.19041.0")
message(WARNING "The target is Windows but CMAKE_SYSTEM_VERSION is not set, the default system version is set to Windows 10.")
endif()
endif()
if(CMAKE_SIZEOF_VOID_P LESS 8)
message(WARNING "Pointer size ${CMAKE_SIZEOF_VOID_P} is not supported. Please use a 64-bit compiler.")
+7 -2
View File
@@ -86,7 +86,9 @@ foreach(_listvar "common;common" "decoder;dec" "encoder;enc")
# Check if the target was already found by Pkgconf
if(TARGET PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
# ALIAS since we don't want the PkgConfig namespace on the Cmake library (for end-users)
add_library(Brotli::${_component_name} ALIAS PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
if (NOT TARGET Brotli::${_component_name})
add_library(Brotli::${_component_name} ALIAS PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
endif()
# Tells HANDLE_COMPONENTS we found the component
set(Brotli_${_component_name}_FOUND TRUE)
@@ -139,7 +141,10 @@ foreach(_listvar "common;common" "decoder;dec" "encoder;enc")
# Tells HANDLE_COMPONENTS we found the component
set(Brotli_${_component_name}_FOUND TRUE)
add_library("Brotli::${_component_name}" UNKNOWN IMPORTED)
if (NOT TARGET Brotli::${_component_name})
add_library("Brotli::${_component_name}" UNKNOWN IMPORTED)
endif()
# Attach the literal library and include dir to the IMPORTED target for the end-user
set_target_properties("Brotli::${_component_name}" PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${Brotli_INCLUDE_DIR}"
+34 -26
View File
@@ -48,32 +48,10 @@ std::string get_error_time_format() {
return ss.str();
}
std::string get_client_ip(const Request &req) {
// Check for X-Forwarded-For header first (common in reverse proxy setups)
auto forwarded_for = req.get_header_value("X-Forwarded-For");
if (!forwarded_for.empty()) {
// Get the first IP if there are multiple
auto comma_pos = forwarded_for.find(',');
if (comma_pos != std::string::npos) {
return forwarded_for.substr(0, comma_pos);
}
return forwarded_for;
}
// Check for X-Real-IP header
auto real_ip = req.get_header_value("X-Real-IP");
if (!real_ip.empty()) { return real_ip; }
// Fallback to remote address (though cpp-httplib doesn't provide this
// directly) For demonstration, we'll use a placeholder
return "127.0.0.1";
}
// NGINX Combined log format:
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
// "$http_referer" "$http_user_agent"
void nginx_access_logger(const Request &req, const Response &res) {
auto remote_addr = get_client_ip(req);
std::string remote_user =
"-"; // cpp-httplib doesn't have built-in auth user tracking
auto time_local = get_time_format();
@@ -86,7 +64,7 @@ void nginx_access_logger(const Request &req, const Response &res) {
if (http_user_agent.empty()) http_user_agent = "-";
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
remote_addr, remote_user, time_local, request,
req.remote_addr, remote_user, time_local, request,
status, body_bytes_sent, http_referer,
http_user_agent)
<< std::endl;
@@ -100,7 +78,6 @@ void nginx_error_logger(const Error &err, const Request *req) {
std::string level = "error";
if (req) {
auto client_ip = get_client_ip(*req);
auto request =
std::format("{} {} {}", req->method, req->path, req->version);
auto host = req->get_header_value("Host");
@@ -108,8 +85,8 @@ void nginx_error_logger(const Error &err, const Request *req) {
std::cerr << std::format("{} [{}] {}, client: {}, request: "
"\"{}\", host: \"{}\"",
time_local, level, to_string(err), client_ip,
request, host)
time_local, level, to_string(err),
req->remote_addr, request, host)
<< std::endl;
} else {
// If no request context, just log the error
@@ -131,6 +108,10 @@ void print_usage(const char *program_name) {
std::cout << " Format: mount_point:document_root"
<< std::endl;
std::cout << " (default: /:./html)" << std::endl;
std::cout << " --trusted-proxy <ip> Add trusted proxy IP address"
<< std::endl;
std::cout << " (can be specified multiple times)"
<< std::endl;
std::cout << " --version Show version information"
<< std::endl;
std::cout << " --help Show this help message" << std::endl;
@@ -140,6 +121,9 @@ void print_usage(const char *program_name) {
<< " --host localhost --port 8080 --mount /:./html" << std::endl;
std::cout << " " << program_name
<< " --host 0.0.0.0 --port 3000 --mount /api:./api" << std::endl;
std::cout << " " << program_name
<< " --trusted-proxy 192.168.1.100 --trusted-proxy 10.0.0.1"
<< std::endl;
}
struct ServerConfig {
@@ -147,6 +131,7 @@ struct ServerConfig {
int port = 8080;
std::string mount_point = "/";
std::string document_root = "./html";
std::vector<std::string> trusted_proxies;
};
enum class ParseResult { SUCCESS, HELP_REQUESTED, VERSION_REQUESTED, ERROR };
@@ -205,6 +190,14 @@ ParseResult parse_command_line(int argc, char *argv[], ServerConfig &config) {
} else if (strcmp(argv[i], "--version") == 0) {
std::cout << CPPHTTPLIB_VERSION << std::endl;
return ParseResult::VERSION_REQUESTED;
} else if (strcmp(argv[i], "--trusted-proxy") == 0) {
if (i + 1 >= argc) {
std::cerr << "Error: --trusted-proxy requires an IP address argument"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.trusted_proxies.push_back(argv[++i]);
} else {
std::cerr << "Error: Unknown option '" << argv[i] << "'" << std::endl;
print_usage(argv[0]);
@@ -218,6 +211,11 @@ bool setup_server(Server &svr, const ServerConfig &config) {
svr.set_logger(nginx_access_logger);
svr.set_error_logger(nginx_error_logger);
// Set trusted proxies if specified
if (!config.trusted_proxies.empty()) {
svr.set_trusted_proxies(config.trusted_proxies);
}
auto ret = svr.set_mount_point(config.mount_point, config.document_root);
if (!ret) {
std::cerr
@@ -285,6 +283,16 @@ int main(int argc, char *argv[]) {
<< std::endl;
std::cout << "Mount point: " << config.mount_point << " -> "
<< config.document_root << std::endl;
if (!config.trusted_proxies.empty()) {
std::cout << "Trusted proxies: ";
for (size_t i = 0; i < config.trusted_proxies.size(); ++i) {
if (i > 0) std::cout << ", ";
std::cout << config.trusted_proxies[i];
}
std::cout << std::endl;
}
std::cout << "Press Ctrl+C to shutdown gracefully..." << std::endl;
auto ret = svr.listen(config.hostname, config.port);
+11 -6
View File
@@ -14,11 +14,18 @@ class EventDispatcher {
public:
EventDispatcher() {}
void wait_event(DataSink *sink) {
bool wait_event(DataSink *sink) {
unique_lock<mutex> lk(m_);
int id = id_;
cv_.wait(lk, [&] { return cid_ == id; });
// Wait with timeout to prevent hanging if client disconnects
if (!cv_.wait_for(lk, std::chrono::seconds(5),
[&] { return cid_ == id; })) {
return false; // Timeout occurred
}
sink->write(message_.data(), message_.size());
return true;
}
void send_event(const string &message) {
@@ -71,8 +78,7 @@ int main(void) {
cout << "connected to event1..." << endl;
res.set_chunked_content_provider("text/event-stream",
[&](size_t /*offset*/, DataSink &sink) {
ed.wait_event(&sink);
return true;
return ed.wait_event(&sink);
});
});
@@ -80,8 +86,7 @@ int main(void) {
cout << "connected to event2..." << endl;
res.set_chunked_content_provider("text/event-stream",
[&](size_t /*offset*/, DataSink &sink) {
ed.wait_event(&sink);
return true;
return ed.wait_event(&sink);
});
});
+236 -71
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.26.0"
#define CPPHTTPLIB_VERSION_NUM "0x001A00"
#define CPPHTTPLIB_VERSION "0.27.0"
#define CPPHTTPLIB_VERSION_NUM "0x001B00"
/*
* Platform compatibility check
@@ -1052,6 +1052,9 @@ private:
ssize_t write_headers(Stream &strm, const Headers &headers);
std::string make_host_and_port_string(const std::string &host, int port,
bool is_ssl);
} // namespace detail
class Server {
@@ -1129,6 +1132,8 @@ public:
Server &
set_header_writer(std::function<ssize_t(Stream &, Headers &)> const &writer);
Server &set_trusted_proxies(const std::vector<std::string> &proxies);
Server &set_keep_alive_max_count(size_t count);
Server &set_keep_alive_timeout(time_t sec);
@@ -1167,6 +1172,9 @@ protected:
const std::function<void(Request &)> &setup_request);
std::atomic<socket_t> svr_sock_{INVALID_SOCKET};
std::vector<std::string> trusted_proxies_;
size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT;
time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND;
time_t read_timeout_sec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND;
@@ -1719,8 +1727,6 @@ private:
const std::string &boundary, const UploadFormDataItems &items,
const FormDataProviderItems &provider_items) const;
std::string adjust_host_string(const std::string &host) const;
virtual bool
process_socket(const Socket &socket,
std::chrono::time_point<std::chrono::steady_clock> start_time,
@@ -1953,14 +1959,17 @@ public:
void update_certs(X509 *cert, EVP_PKEY *private_key,
X509_STORE *client_ca_cert_store = nullptr);
int ssl_last_error() const { return last_ssl_error_; }
private:
bool process_and_close_socket(socket_t sock) override;
STACK_OF(X509_NAME) * extract_ca_names_from_x509_store(X509_STORE *store);
SSL_CTX *ctx_;
std::mutex ctx_mutex_;
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
int last_ssl_error_ = 0;
#endif
};
class SSLClient final : public ClientImpl {
@@ -3798,31 +3807,42 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
}
#else
// Fallback implementation using thread-based timeout for other Unix systems
std::mutex result_mutex;
std::condition_variable result_cv;
auto completed = false;
auto result = EAI_SYSTEM;
struct addrinfo *result_addrinfo = nullptr;
std::thread resolve_thread([&]() {
auto thread_result = getaddrinfo(node, service, hints, &result_addrinfo);
struct GetAddrInfoState {
std::mutex mutex;
std::condition_variable result_cv;
bool completed = false;
int result = EAI_SYSTEM;
std::string node = node;
std::string service = service;
struct addrinfo hints = hints;
struct addrinfo *info = nullptr;
};
std::lock_guard<std::mutex> lock(result_mutex);
result = thread_result;
completed = true;
result_cv.notify_one();
// Allocate on the heap, so the resolver thread can keep using the data.
auto state = std::make_shared<GetAddrInfoState>();
std::thread resolve_thread([=]() {
auto thread_result = getaddrinfo(
state->node.c_str(), state->service.c_str(), hints, &state->info);
std::lock_guard<std::mutex> lock(state->mutex);
state->result = thread_result;
state->completed = true;
state->result_cv.notify_one();
});
// Wait for completion or timeout
std::unique_lock<std::mutex> lock(result_mutex);
auto finished = result_cv.wait_for(lock, std::chrono::seconds(timeout_sec),
[&] { return completed; });
std::unique_lock<std::mutex> lock(state->mutex);
auto finished =
state->result_cv.wait_for(lock, std::chrono::seconds(timeout_sec),
[&] { return state->completed; });
if (finished) {
// Operation completed within timeout
resolve_thread.join();
*res = result_addrinfo;
return result;
*res = state->info;
return state->result;
} else {
// Timeout occurred
resolve_thread.detach(); // Let the thread finish in background
@@ -4585,13 +4605,35 @@ inline bool zstd_decompressor::decompress(const char *data, size_t data_length,
}
#endif
inline bool is_prohibited_header_name(const std::string &name) {
using udl::operator""_t;
switch (str2tag(name)) {
case "REMOTE_ADDR"_t:
case "REMOTE_PORT"_t:
case "LOCAL_ADDR"_t:
case "LOCAL_PORT"_t: return true;
default: return false;
}
}
inline bool has_header(const Headers &headers, const std::string &key) {
if (is_prohibited_header_name(key)) { return false; }
return headers.find(key) != headers.end();
}
inline const char *get_header_value(const Headers &headers,
const std::string &key, const char *def,
size_t id) {
if (is_prohibited_header_name(key)) {
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
std::string msg = "Prohibited header name '" + key + "' is specified.";
throw std::invalid_argument(msg);
#else
return "";
#endif
}
auto rng = headers.equal_range(key);
auto it = rng.first;
std::advance(it, static_cast<ssize_t>(id));
@@ -7250,6 +7292,30 @@ inline bool RegexMatcher::match(Request &request) const {
return std::regex_match(request.path, request.matches, regex_);
}
inline std::string make_host_and_port_string(const std::string &host, int port,
bool is_ssl) {
std::string result;
// Enclose IPv6 address in brackets (but not if already enclosed)
if (host.find(':') == std::string::npos ||
(!host.empty() && host[0] == '[')) {
// IPv4, hostname, or already bracketed IPv6
result = host;
} else {
// IPv6 address without brackets
result = "[" + host + "]";
}
// Append port if not default
if ((!is_ssl && port == 80) || (is_ssl && port == 443)) {
; // do nothing
} else {
result += ":" + std::to_string(port);
}
return result;
}
} // namespace detail
// HTTP server implementation
@@ -7462,6 +7528,12 @@ inline Server &Server::set_header_writer(
return *this;
}
inline Server &
Server::set_trusted_proxies(const std::vector<std::string> &proxies) {
trusted_proxies_ = proxies;
return *this;
}
inline Server &Server::set_keep_alive_max_count(size_t count) {
keep_alive_max_count_ = count;
return *this;
@@ -8250,6 +8322,40 @@ inline bool Server::dispatch_request_for_content_reader(
return false;
}
inline std::string
get_client_ip(const std::string &x_forwarded_for,
const std::vector<std::string> &trusted_proxies) {
// X-Forwarded-For is a comma-separated list per RFC 7239
std::vector<std::string> ip_list;
detail::split(x_forwarded_for.data(),
x_forwarded_for.data() + x_forwarded_for.size(), ',',
[&](const char *b, const char *e) {
auto r = detail::trim(b, e, 0, static_cast<size_t>(e - b));
ip_list.emplace_back(std::string(b + r.first, b + r.second));
});
for (size_t i = 0; i < ip_list.size(); ++i) {
auto ip = ip_list[i];
auto is_trusted_proxy =
std::any_of(trusted_proxies.begin(), trusted_proxies.end(),
[&](const std::string &proxy) { return ip == proxy; });
if (is_trusted_proxy) {
if (i == 0) {
// If the trusted proxy is the first IP, there's no preceding client IP
return ip;
} else {
// Return the IP immediately before the trusted proxy
return ip_list[i - 1];
}
}
}
// If no trusted proxy is found, return the first IP in the list
return ip_list.front();
}
inline bool
Server::process_request(Stream &strm, const std::string &remote_addr,
int remote_port, const std::string &local_addr,
@@ -8313,15 +8419,16 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
connection_closed = true;
}
req.remote_addr = remote_addr;
if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) {
auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_);
} else {
req.remote_addr = remote_addr;
}
req.remote_port = remote_port;
req.set_header("REMOTE_ADDR", req.remote_addr);
req.set_header("REMOTE_PORT", std::to_string(req.remote_port));
req.local_addr = local_addr;
req.local_port = local_port;
req.set_header("LOCAL_ADDR", req.local_addr);
req.set_header("LOCAL_PORT", std::to_string(req.local_port));
if (req.has_header("Accept")) {
const auto &accept_header = req.get_header_value("Accept");
@@ -8511,7 +8618,7 @@ inline ClientImpl::ClientImpl(const std::string &host, int port,
const std::string &client_cert_path,
const std::string &client_key_path)
: host_(detail::escape_abstract_namespace_unix_domain(host)), port_(port),
host_and_port_(adjust_host_string(host_) + ":" + std::to_string(port)),
host_and_port_(detail::make_host_and_port_string(host_, port, is_ssl())),
client_cert_path_(client_cert_path), client_key_path_(client_key_path) {}
inline ClientImpl::~ClientImpl() {
@@ -8692,8 +8799,9 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
{
std::lock_guard<std::mutex> guard(socket_mutex_);
// Set this to false immediately - if it ever gets set to true by the end of
// the request, we know another thread instructed us to close the socket.
// Set this to false immediately - if it ever gets set to true by the end
// of the request, we know another thread instructed us to close the
// socket.
socket_should_be_closed_when_request_is_done_ = false;
auto is_alive = false;
@@ -8709,10 +8817,10 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
#endif
if (!is_alive) {
// Attempt to avoid sigpipe by shutting down non-gracefully if it seems
// like the other side has already closed the connection Also, there
// cannot be any requests in flight from other threads since we locked
// request_mutex_, so safe to close everything immediately
// Attempt to avoid sigpipe by shutting down non-gracefully if it
// seems like the other side has already closed the connection Also,
// there cannot be any requests in flight from other threads since we
// locked request_mutex_, so safe to close everything immediately
const bool shutdown_gracefully = false;
shutdown_ssl(socket_, shutdown_gracefully);
shutdown_socket(socket_);
@@ -9016,7 +9124,8 @@ inline bool ClientImpl::create_redirect_client(
}
}
// New method for robust client setup (based on basic_manual_redirect.cpp logic)
// New method for robust client setup (based on basic_manual_redirect.cpp
// logic)
template <typename ClientType>
inline void ClientImpl::setup_redirect_client(ClientType &client) {
// Copy basic settings first
@@ -9120,18 +9229,8 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
// curl behavior)
if (address_family_ == AF_UNIX) {
req.set_header("Host", "localhost");
} else if (is_ssl()) {
if (port_ == 443) {
req.set_header("Host", host_);
} else {
req.set_header("Host", host_and_port_);
}
} else {
if (port_ == 80) {
req.set_header("Host", host_);
} else {
req.set_header("Host", host_and_port_);
}
req.set_header("Host", host_and_port_);
}
}
@@ -9398,12 +9497,6 @@ inline Result ClientImpl::send_with_content_provider(
#endif
}
inline std::string
ClientImpl::adjust_host_string(const std::string &host) const {
if (host.find(':') != std::string::npos) { return "[" + host + "]"; }
return host;
}
inline void ClientImpl::output_log(const Request &req,
const Response &res) const {
if (logger_) {
@@ -9527,8 +9620,8 @@ inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider(
const FormDataProviderItems &provider_items) const {
size_t cur_item = 0;
size_t cur_start = 0;
// cur_item and cur_start are copied to within the std::function and maintain
// state between successive calls
// cur_item and cur_start are copied to within the std::function and
// maintain state between successive calls
return [&, cur_item, cur_start](size_t offset,
DataSink &sink) mutable -> bool {
if (!offset && !items.empty()) {
@@ -10240,8 +10333,8 @@ inline void ClientImpl::stop() {
// If there is anything ongoing right now, the ONLY thread-safe thing we can
// do is to shutdown_socket, so that threads using this socket suddenly
// discover they can't read/write any more and error out. Everything else
// (closing the socket, shutting ssl down) is unsafe because these actions are
// not thread-safe.
// (closing the socket, shutting ssl down) is unsafe because these actions
// are not thread-safe.
if (socket_requests_in_flight_ > 0) {
shutdown_socket(socket_);
@@ -10694,6 +10787,19 @@ inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path,
SSL_CTX_load_verify_locations(ctx_, client_ca_cert_file_path,
client_ca_cert_dir_path);
// Set client CA list to be sent to clients during TLS handshake
if (client_ca_cert_file_path) {
auto ca_list = SSL_load_client_CA_file(client_ca_cert_file_path);
if (ca_list != nullptr) {
SSL_CTX_set_client_CA_list(ctx_, ca_list);
} else {
// Failed to load client CA list, but we continue since
// SSL_CTX_load_verify_locations already succeeded and
// certificate verification will still work
last_ssl_error_ = static_cast<int>(ERR_get_error());
}
}
SSL_CTX_set_verify(
ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
}
@@ -10718,6 +10824,15 @@ inline SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key,
} else if (client_ca_cert_store) {
SSL_CTX_set_cert_store(ctx_, client_ca_cert_store);
// Extract CA names from the store and set them as the client CA list
auto ca_list = extract_ca_names_from_x509_store(client_ca_cert_store);
if (ca_list) {
SSL_CTX_set_client_CA_list(ctx_, ca_list);
} else {
// Failed to extract CA names, record the error
last_ssl_error_ = static_cast<int>(ERR_get_error());
}
SSL_CTX_set_verify(
ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
}
@@ -10798,6 +10913,44 @@ inline bool SSLServer::process_and_close_socket(socket_t sock) {
return ret;
}
inline STACK_OF(X509_NAME) * SSLServer::extract_ca_names_from_x509_store(
X509_STORE *store) {
if (!store) { return nullptr; }
auto ca_list = sk_X509_NAME_new_null();
if (!ca_list) { return nullptr; }
// Get all objects from the store
auto objs = X509_STORE_get0_objects(store);
if (!objs) {
sk_X509_NAME_free(ca_list);
return nullptr;
}
// Iterate through objects and extract certificate subject names
for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) {
auto obj = sk_X509_OBJECT_value(objs, i);
if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
auto cert = X509_OBJECT_get0_X509(obj);
if (cert) {
auto subject = X509_get_subject_name(cert);
if (subject) {
auto name_dup = X509_NAME_dup(subject);
if (name_dup) { sk_X509_NAME_push(ca_list, name_dup); }
}
}
}
}
// If no names were extracted, free the list and return nullptr
if (sk_X509_NAME_num(ca_list) == 0) {
sk_X509_NAME_free(ca_list);
return nullptr;
}
return ca_list;
}
// SSL HTTP client implementation
inline SSLClient::SSLClient(const std::string &host)
: SSLClient(host, 443, std::string(), std::string()) {}
@@ -10878,7 +11031,8 @@ inline void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) {
if (ca_cert_store) {
if (ctx_) {
if (SSL_CTX_get_cert_store(ctx_) != ca_cert_store) {
// Free memory allocated for old cert and use new store `ca_cert_store`
// Free memory allocated for old cert and use new store
// `ca_cert_store`
SSL_CTX_set_cert_store(ctx_, ca_cert_store);
ca_cert_store_ = ca_cert_store;
}
@@ -10900,10 +11054,15 @@ inline long SSLClient::get_openssl_verify_result() const {
inline SSL_CTX *SSLClient::ssl_context() const { return ctx_; }
inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) {
return is_valid() && ClientImpl::create_and_connect_socket(socket, error);
if (!is_valid()) {
error = Error::SSLConnection;
return false;
}
return ClientImpl::create_and_connect_socket(socket, error);
}
// Assumes that socket_mutex_ is locked and that there are no requests in flight
// Assumes that socket_mutex_ is locked and that there are no requests in
// flight
inline bool SSLClient::connect_with_proxy(
Socket &socket,
std::chrono::time_point<std::chrono::steady_clock> start_time,
@@ -11117,6 +11276,11 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
return true;
}
if (ctx_ == nullptr) {
error = Error::SSLConnection;
last_openssl_error_ = ERR_get_error();
}
shutdown_socket(socket);
close_socket(socket);
return false;
@@ -11210,21 +11374,22 @@ SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {
for (decltype(count) i = 0; i < count && !dsn_matched; i++) {
auto val = sk_GENERAL_NAME_value(alt_names, i);
if (val->type == type) {
auto name =
reinterpret_cast<const char *>(ASN1_STRING_get0_data(val->d.ia5));
auto name_len = static_cast<size_t>(ASN1_STRING_length(val->d.ia5));
if (!val || val->type != type) { continue; }
switch (type) {
case GEN_DNS: dsn_matched = check_host_name(name, name_len); break;
auto name =
reinterpret_cast<const char *>(ASN1_STRING_get0_data(val->d.ia5));
if (name == nullptr) { continue; }
case GEN_IPADD:
if (!memcmp(&addr6, name, addr_len) ||
!memcmp(&addr, name, addr_len)) {
ip_matched = true;
}
break;
auto name_len = static_cast<size_t>(ASN1_STRING_length(val->d.ia5));
switch (type) {
case GEN_DNS: dsn_matched = check_host_name(name, name_len); break;
case GEN_IPADD:
if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) {
ip_matched = true;
}
break;
}
}
+4 -2
View File
@@ -108,9 +108,11 @@ subdir('www')
subdir('www2'/'dir')
subdir('www3'/'dir')
# GoogleTest 1.13.0 requires C++14
# New GoogleTest versions require new C++ standards
test_options = []
if gtest_dep.version().version_compare('>=1.13.0')
if gtest_dep.version().version_compare('>=1.17.0')
test_options += 'cpp_std=c++17'
elif gtest_dep.version().version_compare('>=1.13.0')
test_options += 'cpp_std=c++14'
endif
+694 -57
View File
@@ -11,8 +11,10 @@
#endif
#include <gtest/gtest.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <fstream>
#include <future>
#include <limits>
@@ -77,6 +79,73 @@ static void read_file(const std::string &path, std::string &out) {
fs.read(&out[0], static_cast<std::streamsize>(size));
}
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 se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(host, port);
// Warm-up request to establish connection and resolve DNS
auto warmup_res = cli.Get("/benchmark");
ASSERT_TRUE(warmup_res); // Ensure server is responding correctly
// Run multiple trials and collect timings
const int num_trials = 20;
std::vector<int64_t> timings;
timings.reserve(num_trials);
for (int i = 0; i < num_trials; i++) {
auto start = std::chrono::high_resolution_clock::now();
auto res = cli.Get("/benchmark");
auto end = std::chrono::high_resolution_clock::now();
auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
.count();
// Assertions after timing measurement to avoid overhead
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
timings.push_back(elapsed);
}
// Calculate 25th percentile (lower quartile)
std::sort(timings.begin(), timings.end());
auto p25 = timings[num_trials / 4];
// Format timings for output
std::ostringstream timings_str;
timings_str << "[";
for (size_t i = 0; i < timings.size(); i++) {
if (i > 0) timings_str << ", ";
timings_str << timings[i];
}
timings_str << "]";
// Localhost HTTP GET should be fast even in CI environments
EXPECT_LE(p25, 5) << "25th percentile performance is too slow: " << p25
<< "ms (Issue #1777). Timings: " << timings_str.str();
}
TEST(BenchmarkTest, localhost) { performance_test("localhost"); }
TEST(BenchmarkTest, v6) { performance_test("::1"); }
class UnixSocketTest : public ::testing::Test {
protected:
void TearDown() override { std::remove(pathname_.c_str()); }
@@ -1331,6 +1400,27 @@ TEST(RangeTest, FromHTTPBin_Online) {
}
}
TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
auto host = "unresolvableaddress.local";
auto path = std::string{"/"};
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
auto port = 443;
SSLClient cli(host, port);
#else
auto port = 80;
Client cli(host, port);
#endif
cli.set_connection_timeout(1);
{
auto res = cli.Get(path);
ASSERT_FALSE(res);
}
std::this_thread::sleep_for(std::chrono::seconds(8));
}
TEST(ConnectionErrorTest, InvalidHost) {
auto host = "-abcde.com";
@@ -3010,21 +3100,20 @@ protected:
#endif
.Get("/remote_addr",
[&](const Request &req, Response &res) {
auto remote_addr = req.headers.find("REMOTE_ADDR")->second;
EXPECT_TRUE(req.has_header("REMOTE_PORT"));
EXPECT_EQ(req.remote_addr, req.get_header_value("REMOTE_ADDR"));
EXPECT_EQ(req.remote_port,
std::stoi(req.get_header_value("REMOTE_PORT")));
res.set_content(remote_addr.c_str(), "text/plain");
ASSERT_FALSE(req.has_header("REMOTE_ADDR"));
ASSERT_FALSE(req.has_header("REMOTE_PORT"));
ASSERT_ANY_THROW(req.get_header_value("REMOTE_ADDR"));
ASSERT_ANY_THROW(req.get_header_value("REMOTE_PORT"));
res.set_content(req.remote_addr, "text/plain");
})
.Get("/local_addr",
[&](const Request &req, Response &res) {
EXPECT_TRUE(req.has_header("LOCAL_PORT"));
EXPECT_TRUE(req.has_header("LOCAL_ADDR"));
auto local_addr = req.get_header_value("LOCAL_ADDR");
auto local_port = req.get_header_value("LOCAL_PORT");
EXPECT_EQ(req.local_addr, local_addr);
EXPECT_EQ(req.local_port, std::stoi(local_port));
ASSERT_FALSE(req.has_header("LOCAL_ADDR"));
ASSERT_FALSE(req.has_header("LOCAL_PORT"));
ASSERT_ANY_THROW(req.get_header_value("LOCAL_ADDR"));
ASSERT_ANY_THROW(req.get_header_value("LOCAL_PORT"));
auto local_addr = req.local_addr;
auto local_port = std::to_string(req.local_port);
res.set_content(local_addr.append(":").append(local_port),
"text/plain");
})
@@ -3612,46 +3701,6 @@ TEST_F(ServerTest, GetMethod200) {
EXPECT_EQ("Hello World!", res->body);
}
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 se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(host, port);
auto start = std::chrono::high_resolution_clock::now();
auto res = cli.Get("/benchmark");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
auto end = std::chrono::high_resolution_clock::now();
auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
.count();
EXPECT_LE(elapsed, 5) << "Performance is too slow: " << elapsed
<< "ms (Issue #1777)";
}
TEST(BenchmarkTest, localhost) { performance_test("localhost"); }
TEST(BenchmarkTest, v6) { performance_test("::1"); }
TEST_F(ServerTest, GetEmptyFile) {
auto res = cli_.Get("/empty_file");
ASSERT_TRUE(res);
@@ -8345,6 +8394,61 @@ TEST(SSLClientTest, Issue2004_Online) {
EXPECT_EQ(body.substr(0, 15), "<!doctype html>");
}
TEST(SSLClientTest, ErrorReportingWhenInvalid) {
// Create SSLClient with invalid cert/key to make is_valid() return false
SSLClient cli("localhost", 8080, "nonexistent_cert.pem",
"nonexistent_key.pem");
// is_valid() should be false due to cert loading failure
ASSERT_FALSE(cli.is_valid());
auto res = cli.Get("/");
ASSERT_FALSE(res);
EXPECT_EQ(Error::SSLConnection, res.error());
}
TEST(SSLClientTest, Issue2251_SwappedClientCertAndKey) {
// Test for Issue #2251: SSL error not properly reported when client cert
// and key paths are swapped or mismatched
// This simulates the scenario where user accidentally swaps the cert and key
// files
// Using client cert file as private key and vice versa (completely wrong)
SSLClient cli("localhost", 8080, "client.key.pem", "client.cert.pem");
// Should fail validation due to cert/key mismatch
ASSERT_FALSE(cli.is_valid());
// Attempt to make a request should fail with proper error
auto res = cli.Get("/");
ASSERT_FALSE(res);
EXPECT_EQ(Error::SSLConnection, res.error());
// SSL error should be recorded in the Result object (this is the key fix for
// Issue #2251)
auto openssl_error = res.ssl_openssl_error();
EXPECT_NE(0u, openssl_error);
}
TEST(SSLClientTest, Issue2251_ClientCertFileNotMatchingKey) {
// Another variant: using valid file paths but with mismatched cert/key pair
// This tests the case where files exist but contain incompatible key material
// Using client cert with wrong key (cert2 key)
SSLClient cli("localhost", 8080, "client.cert.pem", "key.pem");
// Should fail validation
ASSERT_FALSE(cli.is_valid());
auto res = cli.Get("/");
ASSERT_FALSE(res);
// Must report error properly, not appear as success
EXPECT_EQ(Error::SSLConnection, res.error());
// OpenSSL error should be captured in Result
EXPECT_NE(0u, res.ssl_openssl_error());
}
#if 0
TEST(SSLClientTest, SetInterfaceWithINET6) {
auto cli = std::make_shared<httplib::Client>("https://httpbin.org");
@@ -8687,6 +8791,197 @@ TEST(SSLClientServerTest, CustomizeServerSSLCtx) {
ASSERT_EQ(StatusCode::OK_200, res->status);
}
TEST(SSLClientServerTest, ClientCAListSentToClient) {
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE);
ASSERT_TRUE(svr.is_valid());
// Set up a handler to verify client certificate is present
bool client_cert_verified = false;
svr.Get("/test", [&](const Request & /*req*/, Response &res) {
// Verify that client certificate was provided
client_cert_verified = true;
res.set_content("success", "text/plain");
});
thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
// Client with certificate
SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
cli.enable_server_certificate_verification(false);
cli.set_connection_timeout(30);
auto res = cli.Get("/test");
ASSERT_TRUE(res);
ASSERT_EQ(StatusCode::OK_200, res->status);
ASSERT_TRUE(client_cert_verified);
EXPECT_EQ("success", res->body);
}
TEST(SSLClientServerTest, ClientCAListSetInContext) {
// Test that when client CA cert file is provided,
// SSL_CTX_set_client_CA_list is called and the CA list is properly set
// Create a server with client authentication
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE);
ASSERT_TRUE(svr.is_valid());
// We can't directly access the SSL_CTX from SSLServer to verify,
// but we can test that the server properly requests client certificates
// and accepts valid ones from the specified CA
bool handler_called = false;
svr.Get("/test", [&](const Request &req, Response &res) {
handler_called = true;
// Verify that a client certificate was provided
auto peer_cert = SSL_get_peer_certificate(req.ssl);
ASSERT_TRUE(peer_cert != nullptr);
// Get the issuer name
auto issuer_name = X509_get_issuer_name(peer_cert);
ASSERT_TRUE(issuer_name != nullptr);
char issuer_buf[256];
X509_NAME_oneline(issuer_name, issuer_buf, sizeof(issuer_buf));
// The client certificate should be issued by our test CA
std::string issuer_str(issuer_buf);
EXPECT_TRUE(issuer_str.find("Root CA Name") != std::string::npos);
X509_free(peer_cert);
res.set_content("authenticated", "text/plain");
});
thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
// Connect with a client certificate issued by the CA
SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
cli.enable_server_certificate_verification(false);
cli.set_connection_timeout(30);
auto res = cli.Get("/test");
ASSERT_TRUE(res);
ASSERT_EQ(StatusCode::OK_200, res->status);
ASSERT_TRUE(handler_called);
EXPECT_EQ("authenticated", res->body);
}
TEST(SSLClientServerTest, ClientCAListLoadErrorRecorded) {
// Test 1: Valid CA file - no error should be recorded
{
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE,
CLIENT_CA_CERT_FILE);
ASSERT_TRUE(svr.is_valid());
// With valid setup, last_ssl_error should be 0
EXPECT_EQ(0, svr.ssl_last_error());
}
// Test 2: Invalid CA file content
// When SSL_load_client_CA_file fails, last_ssl_error_ should be set
{
// Create a temporary file with completely invalid content
const char *temp_invalid_ca = "./temp_invalid_ca_for_test.txt";
{
std::ofstream ofs(temp_invalid_ca);
ofs << "This is not a certificate file at all\n";
ofs << "Just plain text content\n";
}
// Create server with invalid CA file
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, temp_invalid_ca);
// Clean up temporary file
std::remove(temp_invalid_ca);
// When there's an SSL error (from either SSL_CTX_load_verify_locations
// or SSL_load_client_CA_file), last_ssl_error_ should be non-zero
// Note: SSL_CTX_load_verify_locations typically fails first,
// but our error handling code path is still exercised
if (!svr.is_valid()) { EXPECT_NE(0, svr.ssl_last_error()); }
}
}
TEST(SSLClientServerTest, ClientCAListFromX509Store) {
// Test SSL server using X509_STORE constructor with client CA certificates
// This test verifies that Phase 2 implementation correctly extracts CA names
// from an X509_STORE and sets them in the SSL context
// Load the CA certificate into memory
auto bio = BIO_new_file(CLIENT_CA_CERT_FILE, "r");
ASSERT_NE(nullptr, bio);
auto ca_cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_free(bio);
ASSERT_NE(nullptr, ca_cert);
// Create an X509_STORE and add the CA certificate
auto store = X509_STORE_new();
ASSERT_NE(nullptr, store);
ASSERT_EQ(1, X509_STORE_add_cert(store, ca_cert));
// Load server certificate and private key
auto cert_bio = BIO_new_file(SERVER_CERT_FILE, "r");
ASSERT_NE(nullptr, cert_bio);
auto server_cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr);
BIO_free(cert_bio);
ASSERT_NE(nullptr, server_cert);
auto key_bio = BIO_new_file(SERVER_PRIVATE_KEY_FILE, "r");
ASSERT_NE(nullptr, key_bio);
auto server_key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr);
BIO_free(key_bio);
ASSERT_NE(nullptr, server_key);
// Create SSLServer with X509_STORE constructor
// Note: X509_STORE ownership is transferred to SSL_CTX
SSLServer svr(server_cert, server_key, store);
ASSERT_TRUE(svr.is_valid());
// No SSL error should be recorded for valid setup
EXPECT_EQ(0, svr.ssl_last_error());
// Set up server endpoints
svr.Get("/test-x509store", [&](const Request & /*req*/, Response &res) {
res.set_content("ok", "text/plain");
});
// Start server in a thread
auto server_thread = thread([&]() { svr.listen(HOST, PORT); });
svr.wait_until_ready();
// Connect with client certificate (using constructor with paths)
SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
cli.enable_server_certificate_verification(false);
auto res = cli.Get("/test-x509store");
ASSERT_TRUE(res);
EXPECT_EQ(200, res->status);
EXPECT_EQ("ok", res->body);
// Clean up
X509_free(server_cert);
EVP_PKEY_free(server_key);
X509_free(ca_cert);
svr.stop();
server_thread.join();
}
// Disabled due to the out-of-memory problem on GitHub Actions Workflows
TEST(SSLClientServerTest, DISABLED_LargeDataTransfer) {
@@ -10298,6 +10593,103 @@ TEST(FileSystemTest, FileAndDirExistenceCheck) {
EXPECT_TRUE(stat_dir.is_dir());
}
TEST(MakeHostAndPortStringTest, VariousPatterns) {
// IPv4 with default HTTP port (80)
EXPECT_EQ("example.com",
detail::make_host_and_port_string("example.com", 80, false));
// IPv4 with default HTTPS port (443)
EXPECT_EQ("example.com",
detail::make_host_and_port_string("example.com", 443, true));
// IPv4 with non-default HTTP port
EXPECT_EQ("example.com:8080",
detail::make_host_and_port_string("example.com", 8080, false));
// IPv4 with non-default HTTPS port
EXPECT_EQ("example.com:8443",
detail::make_host_and_port_string("example.com", 8443, true));
// IPv6 with default HTTP port (80)
EXPECT_EQ("[::1]", detail::make_host_and_port_string("::1", 80, false));
// IPv6 with default HTTPS port (443)
EXPECT_EQ("[::1]", detail::make_host_and_port_string("::1", 443, true));
// IPv6 with non-default HTTP port
EXPECT_EQ("[::1]:8080",
detail::make_host_and_port_string("::1", 8080, false));
// IPv6 with non-default HTTPS port
EXPECT_EQ("[::1]:8443", detail::make_host_and_port_string("::1", 8443, true));
// IPv6 full address with default port
EXPECT_EQ("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
detail::make_host_and_port_string(
"2001:0db8:85a3:0000:0000:8a2e:0370:7334", 443, true));
// IPv6 full address with non-default port
EXPECT_EQ("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:9000",
detail::make_host_and_port_string(
"2001:0db8:85a3:0000:0000:8a2e:0370:7334", 9000, false));
// IPv6 localhost with non-default port
EXPECT_EQ("[::1]:3000",
detail::make_host_and_port_string("::1", 3000, false));
// IPv6 with zone ID (link-local address) with default port
EXPECT_EQ("[fe80::1%eth0]",
detail::make_host_and_port_string("fe80::1%eth0", 80, false));
// IPv6 with zone ID (link-local address) with non-default port
EXPECT_EQ("[fe80::1%eth0]:8080",
detail::make_host_and_port_string("fe80::1%eth0", 8080, false));
// Edge case: Port 443 with is_ssl=false (should add port)
EXPECT_EQ("example.com:443",
detail::make_host_and_port_string("example.com", 443, false));
// Edge case: Port 80 with is_ssl=true (should add port)
EXPECT_EQ("example.com:80",
detail::make_host_and_port_string("example.com", 80, true));
// IPv6 edge case: Port 443 with is_ssl=false (should add port)
EXPECT_EQ("[::1]:443", detail::make_host_and_port_string("::1", 443, false));
// IPv6 edge case: Port 80 with is_ssl=true (should add port)
EXPECT_EQ("[::1]:80", detail::make_host_and_port_string("::1", 80, true));
// Security fix: Already bracketed IPv6 should not get double brackets
EXPECT_EQ("[::1]", detail::make_host_and_port_string("[::1]", 80, false));
EXPECT_EQ("[::1]", detail::make_host_and_port_string("[::1]", 443, true));
EXPECT_EQ("[::1]:8080",
detail::make_host_and_port_string("[::1]", 8080, false));
EXPECT_EQ("[2001:db8::1]:8080",
detail::make_host_and_port_string("[2001:db8::1]", 8080, false));
EXPECT_EQ("[fe80::1%eth0]",
detail::make_host_and_port_string("[fe80::1%eth0]", 80, false));
EXPECT_EQ("[fe80::1%eth0]:8080",
detail::make_host_and_port_string("[fe80::1%eth0]", 8080, false));
// Edge case: Empty host (should return as-is)
EXPECT_EQ("", detail::make_host_and_port_string("", 80, false));
// Edge case: Colon in hostname (non-IPv6) - will be treated as IPv6
// This is a known limitation but shouldn't crash
EXPECT_EQ("[host:name]",
detail::make_host_and_port_string("host:name", 80, false));
// Port number edge cases (no validation, but should not crash)
EXPECT_EQ("example.com:0",
detail::make_host_and_port_string("example.com", 0, false));
EXPECT_EQ("example.com:-1",
detail::make_host_and_port_string("example.com", -1, false));
EXPECT_EQ("example.com:65535",
detail::make_host_and_port_string("example.com", 65535, false));
EXPECT_EQ("example.com:65536",
detail::make_host_and_port_string("example.com", 65536, false));
}
TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) {
Server svr;
@@ -10678,11 +11070,18 @@ class EventDispatcher {
public:
EventDispatcher() {}
void wait_event(DataSink *sink) {
bool wait_event(DataSink *sink) {
unique_lock<mutex> lk(m_);
int id = id_;
cv_.wait(lk, [&] { return cid_ == id; });
// Wait with timeout to prevent hanging if client disconnects
if (!cv_.wait_for(lk, std::chrono::seconds(5),
[&] { return cid_ == id; })) {
return false; // Timeout occurred
}
sink->write(message_.data(), message_.size());
return true;
}
void send_event(const string &message) {
@@ -10707,8 +11106,7 @@ TEST(ClientInThreadTest, Issue2068) {
svr.Get("/event1", [&](const Request & /*req*/, Response &res) {
res.set_chunked_content_provider("text/event-stream",
[&](size_t /*offset*/, DataSink &sink) {
ed.wait_event(&sink);
return true;
return ed.wait_event(&sink);
});
});
@@ -10751,9 +11149,11 @@ TEST(ClientInThreadTest, Issue2068) {
std::this_thread::sleep_for(std::chrono::seconds(2));
stop = true;
client->stop();
client.reset();
t.join();
// Reset client after thread has finished
client.reset();
}
}
@@ -10807,3 +11207,240 @@ TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) {
std::string res;
ASSERT_TRUE(send_request(1, req, &res));
}
TEST(ForwardedHeadersTest, NoProxiesSetting) {
Server svr;
std::string observed_remote_addr;
std::string observed_xff;
svr.Get("/ip", [&](const Request &req, Response &res) {
observed_remote_addr = req.remote_addr;
observed_xff = req.get_header_value("X-Forwarded-For");
res.set_content("ok", "text/plain");
});
thread t = thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
auto res = cli.Get("/ip", {{"X-Forwarded-For", "203.0.113.66"}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(observed_xff, "203.0.113.66");
EXPECT_TRUE(observed_remote_addr == "::1" || observed_remote_addr == "127.0.0.1");
}
TEST(ForwardedHeadersTest, NoForwardedHeaders) {
Server svr;
svr.set_trusted_proxies({"203.0.113.66"});
std::string observed_remote_addr;
std::string observed_xff;
svr.Get("/ip", [&](const Request &req, Response &res) {
observed_remote_addr = req.remote_addr;
observed_xff = req.get_header_value("X-Forwarded-For");
res.set_content("ok", "text/plain");
});
thread t = thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
auto res = cli.Get("/ip");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(observed_xff, "");
EXPECT_TRUE(observed_remote_addr == "::1" || observed_remote_addr == "127.0.0.1");
}
TEST(ForwardedHeadersTest, SingleTrustedProxy_UsesIPBeforeTrusted) {
Server svr;
svr.set_trusted_proxies({"203.0.113.66"});
std::string observed_remote_addr;
std::string observed_xff;
svr.Get("/ip", [&](const Request &req, Response &res) {
observed_remote_addr = req.remote_addr;
observed_xff = req.get_header_value("X-Forwarded-For");
res.set_content("ok", "text/plain");
});
thread t = thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
auto res = cli.Get("/ip", {{"X-Forwarded-For", "198.51.100.23, 203.0.113.66"}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(observed_xff, "198.51.100.23, 203.0.113.66");
EXPECT_EQ(observed_remote_addr, "198.51.100.23");
}
TEST(ForwardedHeadersTest, MultipleTrustedProxies_UsesClientIP) {
Server svr;
svr.set_trusted_proxies({"203.0.113.66", "192.0.2.45"});
std::string observed_remote_addr;
std::string observed_xff;
svr.Get("/ip", [&](const Request &req, Response &res) {
observed_remote_addr = req.remote_addr;
observed_xff = req.get_header_value("X-Forwarded-For");
res.set_content("ok", "text/plain");
});
thread t = thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
auto res = cli.Get(
"/ip",
{{"X-Forwarded-For", "198.51.100.23, 203.0.113.66, 192.0.2.45"}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(observed_xff, "198.51.100.23, 203.0.113.66, 192.0.2.45");
EXPECT_EQ(observed_remote_addr, "198.51.100.23");
}
TEST(ForwardedHeadersTest, TrustedProxyNotInHeader_UsesFirstFromXFF) {
Server svr;
svr.set_trusted_proxies({"192.0.2.45"});
std::string observed_remote_addr;
std::string observed_xff;
svr.Get("/ip", [&](const Request &req, Response &res) {
observed_remote_addr = req.remote_addr;
observed_xff = req.get_header_value("X-Forwarded-For");
res.set_content("ok", "text/plain");
});
thread t = thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
auto res = cli.Get("/ip",
{{"X-Forwarded-For", "198.51.100.23, 198.51.100.24"}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(observed_xff, "198.51.100.23, 198.51.100.24");
EXPECT_EQ(observed_remote_addr, "198.51.100.23");
}
TEST(ForwardedHeadersTest, LastHopTrusted_SelectsImmediateLeftIP) {
Server svr;
svr.set_trusted_proxies({"192.0.2.45"});
std::string observed_remote_addr;
std::string observed_xff;
svr.Get("/ip", [&](const Request &req, Response &res) {
observed_remote_addr = req.remote_addr;
observed_xff = req.get_header_value("X-Forwarded-For");
res.set_content("ok", "text/plain");
});
thread t = thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
auto res = cli.Get(
"/ip",
{{"X-Forwarded-For", "198.51.100.23, 203.0.113.66, 192.0.2.45"}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(observed_xff, "198.51.100.23, 203.0.113.66, 192.0.2.45");
EXPECT_EQ(observed_remote_addr, "203.0.113.66");
}
TEST(ForwardedHeadersTest, HandlesWhitespaceAroundIPs) {
Server svr;
svr.set_trusted_proxies({"192.0.2.45"});
std::string observed_remote_addr;
std::string observed_xff;
svr.Get("/ip", [&](const Request &req, Response &res) {
observed_remote_addr = req.remote_addr;
observed_xff = req.get_header_value("X-Forwarded-For");
res.set_content("ok", "text/plain");
});
thread t = thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
auto res = cli.Get(
"/ip",
{{"X-Forwarded-For", " 198.51.100.23 , 203.0.113.66 , 192.0.2.45 "}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
// 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");
EXPECT_EQ(observed_remote_addr, "203.0.113.66");
}