mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eacc1ca98e | |||
| ac9ebb0ee3 | |||
| 11eed05ce7 | |||
| f3bba0646a | |||
| 2da189f88c | |||
| 6e0f211cff | |||
| 318a3fe425 | |||
| 2d8d524178 | |||
| afa88dbe70 | |||
| 08133b593b | |||
| 8aedbf4547 | |||
| cde29362ef | |||
| bae40fcdf2 | |||
| db561f5552 | |||
| 35c52c1ab9 | |||
| 23ff9a5605 | |||
| 41be1e24e3 | |||
| 6e52d0a057 | |||
| f72b4582e6 |
@@ -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
@@ -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
@@ -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.")
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user