Compare commits

...

43 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
yhirose 89c932f313 Release v0.26.0 2025-08-29 16:05:44 -04:00
yhirose eb5a65e0df Fix #2217 2025-08-29 15:01:59 -04:00
kgokalp 7a3b92bbd9 Fix: handle EAI_ALLDONE from gai_suspend in getaddrinfo_with_timeout (#2228) 2025-08-28 11:08:32 -04:00
yhirose eb11032797 Fix platform problem 2025-08-27 00:31:14 -04:00
yhirose 54e75dc8ef Add manual run 2025-08-26 23:34:18 -04:00
yhirose b20b5fdd1f Add 'release-docker' workflow 2025-08-26 23:18:59 -04:00
yhirose f4cc542d4b Fix Dockerfile problem with CMD 2025-08-26 22:17:54 -04:00
yhirose 4285d33992 Fix #2223 (#2224)
* Fix #2223

* Fix build error
2025-08-26 21:42:13 -04:00
yhirose 92b4f53012 clang-format 2025-08-26 20:29:43 -04:00
tejas b8e21eac89 Initialize start time for server (#2220)
* Initialize start time for server

By initializing start_time_ for server, I hope to measure the time taken to process a request at the end maybe in the set_logger callback and print it.

I only see current usage in client with server retaining the inital min value

* Add test to verify start time is initialized

* Address review comments

* run clang format
2025-08-26 15:34:13 -04:00
Sergey 3fae5f1473 osx: fix inconsistent use of the macro TARGET_OS_OSX (#2222)
* osx: fix inconsistent use of the macro `TARGET_OS_OSX`

Fixed the build error on iOS:

```
httplib.h:3583:3: error: unknown type name 'CFStringRef'
  870 |   CFStringRef hostname_ref = CFStringCreateWithCString(
```

Note, `TARGET_OS_OSX` is defined but is 0 when `TARGET_OS_IOS` is 1,
and vise versa. Hence, `TARGET_OS_MAC` should have been used, that is
set to 1 for the both targets.

* improve: non-blocking getaddrinfo() for all mac target variants

`TARGET_OS_MAC` should have been used, that is set to 1 for all other
targets: OSX, IPHONE (IOS, TV, WATCH, VISION, BRIDGE), SIMULATOR,
DRIVERKIT.
2025-08-26 12:46:51 -04:00
Andrea Pappacoda fe7fe15d2e build(meson): fix new build option names (#2208)
This is a follow-up to commit 4ff7a1c858,
which introduced new simplified build options and deprecated the old
ones. I forgot to also change the various get_option() calls,
effectively rendering the new option names useless, as they would not
get honoured.
2025-08-19 15:22:08 -04:00
Thomas Beutlich fbd6ce7a3f Make code sample compilable (#2207) 2025-08-14 06:57:39 -04:00
Thomas Beutlich dffce89514 #2201 Fix 32-bit MSVC compiler error due to unknown command #warning (#2202) 2025-08-12 17:06:09 -04:00
yhirose 3f44c80fd3 Release v0.25.0 2025-08-07 20:58:39 -04:00
yhirose a2bb6f6c1e Update docker/main.cc 2025-08-07 20:57:37 -04:00
Thomas Beutlich 7012e765e1 CMake: Check pointer size at configure time (#2197)
* Check pointer size at configure time

* Relax check to warning
2025-08-07 17:14:19 -04:00
yhirose b1c1fa2dc6 Code cleanup 2025-08-07 00:09:09 -04:00
yhirose fbee136dca Fix #2193. Allow _WIN32 2025-08-06 23:12:33 -04:00
yhirose 70cca55caf Workaround for chocolatey issue with the latest OpenSSL 2025-08-06 17:54:08 -04:00
yhirose cdaed14925 Update README 2025-08-06 17:41:03 -04:00
yhirose b52d7d8411 ErrorLogger support (#870) (#2195) 2025-08-06 17:38:18 -04:00
Thomas Beutlich acf28a362d #2191 Check for minimum required Windows version (#2192) 2025-08-01 20:16:43 -04:00
yhirose 0b3758ec36 Fix problem with Windows version check 2025-07-30 17:39:40 -04:00
12 changed files with 1761 additions and 285 deletions
+51
View File
@@ -0,0 +1,51 @@
name: Release Docker Image
on:
release:
types: [published]
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history and tags
- name: Extract tag (manual)
if: github.event_name == 'workflow_dispatch'
id: set_tag_manual
run: |
# Checkout the latest tag and set output
git fetch --tags
LATEST_TAG=$(git describe --tags --abbrev=0)
git checkout $LATEST_TAG
echo "tag=${LATEST_TAG#v}" >> $GITHUB_OUTPUT
- name: Extract tag (release)
if: github.event_name == 'release'
id: set_tag_release
run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64 # Build for both amd64 and arm64
# Use extracted tag without leading 'v'
tags: |
yhirose4dockerhub/cpp-httplib-server:latest
yhirose4dockerhub/cpp-httplib-server:${{ steps.set_tag_manual.outputs.tag || steps.set_tag_release.outputs.tag }}
+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
+19 -5
View File
@@ -111,12 +111,26 @@ option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build.
option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
# Defaults to static library
option(BUILD_SHARED_LIBS "Build the library as a shared library instead of static. Has no effect if using header-only." OFF)
if (BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE)
if(BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE)
# Necessary for Windows if building shared libs
# See https://stackoverflow.com/a/40743080
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
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.")
endif()
# Set some variables that are used in-tree and while building based on our options
set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE})
set(HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN ${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN})
@@ -243,8 +257,8 @@ add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11)
target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC}
$<BUILD_INTERFACE:${_httplib_build_includedir}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
$<BUILD_INTERFACE:${_httplib_build_includedir}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
# Always require threads
@@ -340,6 +354,6 @@ if(HTTPLIB_INSTALL)
endif()
if(HTTPLIB_TEST)
include(CTest)
add_subdirectory(test)
include(CTest)
add_subdirectory(test)
endif()
+3 -1
View File
@@ -8,4 +8,6 @@ FROM scratch
COPY --from=builder /build/server /server
COPY docker/html/index.html /html/index.html
EXPOSE 80
CMD ["/server"]
ENTRYPOINT ["/server"]
CMD ["--host", "0.0.0.0", "--port", "80", "--mount", "/:./html"]
+79 -11
View File
@@ -98,32 +98,31 @@ httplib::Client cli("https://example.com");
auto res = cli.Get("/");
if (!res) {
// Check the error type
auto err = res.error();
const auto err = res.error();
switch (err) {
case httplib::Error::SSLConnection:
std::cout << "SSL connection failed, SSL error: "
<< res->ssl_error() << std::endl;
<< res.ssl_error() << std::endl;
break;
case httplib::Error::SSLLoadingCerts:
std::cout << "SSL cert loading failed, OpenSSL error: "
<< std::hex << res->ssl_openssl_error() << std::endl;
<< std::hex << res.ssl_openssl_error() << std::endl;
break;
case httplib::Error::SSLServerVerification:
std::cout << "SSL verification failed, X509 error: "
<< res->ssl_openssl_error() << std::endl;
<< res.ssl_openssl_error() << std::endl;
break;
case httplib::Error::SSLServerHostnameVerification:
std::cout << "SSL hostname verification failed, X509 error: "
<< res->ssl_openssl_error() << std::endl;
<< res.ssl_openssl_error() << std::endl;
break;
default:
std::cout << "HTTP error: " << httplib::to_string(err) << std::endl;
}
}
}
```
@@ -269,23 +268,47 @@ svr.set_file_request_handler([](const Request &req, Response &res) {
### Logging
cpp-httplib provides separate logging capabilities for access logs and error logs, similar to web servers like Nginx and Apache.
#### Access Logging
Access loggers capture successful HTTP requests and responses:
```cpp
svr.set_logger([](const auto& req, const auto& res) {
your_logger(req, res);
svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
std::cout << req.method << " " << req.path << " -> " << res.status << std::endl;
});
```
You can also set a pre-compression logger to capture request/response data before compression is applied. This is useful for debugging and monitoring purposes when you need to see the original, uncompressed response content:
#### Pre-compression Logging
You can also set a pre-compression logger to capture request/response data before compression is applied:
```cpp
svr.set_pre_compression_logger([](const auto& req, const auto& res) {
svr.set_pre_compression_logger([](const httplib::Request& req, const httplib::Response& res) {
// Log before compression - res.body contains uncompressed content
// Content-Encoding header is not yet set
your_pre_compression_logger(req, res);
});
```
The pre-compression logger is only called when compression would be applied. For responses without compression, only the regular logger is called.
The pre-compression logger is only called when compression would be applied. For responses without compression, only the access logger is called.
#### Error Logging
Error loggers capture failed requests and connection issues. Unlike access loggers, error loggers only receive the Error and Request information, as errors typically occur before a meaningful Response can be generated.
```cpp
svr.set_error_logger([](const httplib::Error& err, const httplib::Request* req) {
std::cerr << httplib::to_string(err) << " while processing request";
if (req) {
std::cerr << ", client: " << req->get_header_value("X-Forwarded-For")
<< ", request: '" << req->method << " " << req->path << " " << req->version << "'"
<< ", host: " << req->get_header_value("Host");
}
std::cerr << std::endl;
});
```
### Error handler
@@ -718,6 +741,51 @@ enum Error {
};
```
### Client Logging
#### Access Logging
```cpp
cli.set_logger([](const httplib::Request& req, const httplib::Response& res) {
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_time).count();
std::cout << "✓ " << req.method << " " << req.path
<< " -> " << res.status << " (" << res.body.size() << " bytes, "
<< duration << "ms)" << std::endl;
});
```
#### Error Logging
```cpp
cli.set_error_logger([](const httplib::Error& err, const httplib::Request* req) {
std::cerr << "✗ ";
if (req) {
std::cerr << req->method << " " << req->path << " ";
}
std::cerr << "failed: " << httplib::to_string(err);
// Add specific guidance based on error type
switch (err) {
case httplib::Error::Connection:
std::cerr << " (verify server is running and reachable)";
break;
case httplib::Error::SSLConnection:
std::cerr << " (check SSL certificate and TLS configuration)";
break;
case httplib::Error::ConnectionTimeout:
std::cerr << " (increase timeout or check network latency)";
break;
case httplib::Error::Read:
std::cerr << " (server may have closed connection prematurely)";
break;
default:
break;
}
std::cerr << std::endl;
});
```
### GET with HTTP headers
```c++
+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}"
+264 -42
View File
@@ -5,77 +5,299 @@
// MIT License
//
#include <atomic>
#include <chrono>
#include <ctime>
#include <format>
#include <iomanip>
#include <iostream>
#include <signal.h>
#include <sstream>
#include <httplib.h>
constexpr auto error_html = R"(<html>
<head><title>{} {}</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>cpp-httplib/{}</center>
</body>
</html>
)";
using namespace httplib;
void sigint_handler(int s) { exit(1); }
const auto SERVER_NAME =
std::format("cpp-httplib-server/{}", CPPHTTPLIB_VERSION);
std::string time_local() {
auto p = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(p);
Server svr;
void signal_handler(int signal) {
if (signal == SIGINT || signal == SIGTERM) {
std::cout << "\nReceived signal, shutting down gracefully...\n";
svr.stop();
}
}
std::string get_time_format() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&t), "%d/%b/%Y:%H:%M:%S %z");
ss << std::put_time(std::localtime(&time_t), "%d/%b/%Y:%H:%M:%S %z");
return ss.str();
}
std::string log(auto &req, auto &res) {
auto remote_user = "-"; // TODO:
auto request = std::format("{} {} {}", req.method, req.path, req.version);
auto body_bytes_sent = res.get_header_value("Content-Length");
auto http_referer = "-"; // TODO:
auto http_user_agent = req.get_header_value("User-Agent", "-");
std::string get_error_time_format() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
// NOTE: From NGINX default access log format
// log_format combined '$remote_addr - $remote_user [$time_local] '
// '"$request" $status $body_bytes_sent '
// '"$http_referer" "$http_user_agent"';
return std::format(R"({} - {} [{}] "{}" {} {} "{}" "{}")", req.remote_addr,
remote_user, time_local(), request, res.status,
body_bytes_sent, http_referer, http_user_agent);
std::stringstream ss;
ss << std::put_time(std::localtime(&time_t), "%Y/%m/%d %H:%M:%S");
return ss.str();
}
int main(int argc, const char **argv) {
signal(SIGINT, sigint_handler);
// 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) {
std::string remote_user =
"-"; // cpp-httplib doesn't have built-in auth user tracking
auto time_local = get_time_format();
auto request = std::format("{} {} {}", req.method, req.path, req.version);
auto status = res.status;
auto body_bytes_sent = res.body.size();
auto http_referer = req.get_header_value("Referer");
if (http_referer.empty()) http_referer = "-";
auto http_user_agent = req.get_header_value("User-Agent");
if (http_user_agent.empty()) http_user_agent = "-";
auto base_dir = "./html";
auto host = "0.0.0.0";
auto port = 80;
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
req.remote_addr, remote_user, time_local, request,
status, body_bytes_sent, http_referer,
http_user_agent)
<< std::endl;
}
httplib::Server svr;
// NGINX Error log format:
// YYYY/MM/DD HH:MM:SS [level] message, client: client_ip, request: "request",
// host: "host"
void nginx_error_logger(const Error &err, const Request *req) {
auto time_local = get_error_time_format();
std::string level = "error";
svr.set_error_handler([](auto & /*req*/, auto &res) {
auto body =
std::format(error_html, res.status, httplib::status_message(res.status),
CPPHTTPLIB_VERSION);
if (req) {
auto request =
std::format("{} {} {}", req->method, req->path, req->version);
auto host = req->get_header_value("Host");
if (host.empty()) host = "-";
res.set_content(body, "text/html");
std::cerr << std::format("{} [{}] {}, client: {}, request: "
"\"{}\", host: \"{}\"",
time_local, level, to_string(err),
req->remote_addr, request, host)
<< std::endl;
} else {
// If no request context, just log the error
std::cerr << std::format("{} [{}] {}", time_local, level, to_string(err))
<< std::endl;
}
}
void print_usage(const char *program_name) {
std::cout << "Usage: " << program_name << " [OPTIONS]" << std::endl;
std::cout << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " --host <hostname> Server hostname (default: localhost)"
<< std::endl;
std::cout << " --port <port> Server port (default: 8080)"
<< std::endl;
std::cout << " --mount <mount:path> Mount point and document root"
<< std::endl;
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;
std::cout << std::endl;
std::cout << "Examples:" << std::endl;
std::cout << " " << 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 {
std::string hostname = "localhost";
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 };
ParseResult parse_command_line(int argc, char *argv[], ServerConfig &config) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
print_usage(argv[0]);
return ParseResult::HELP_REQUESTED;
} else if (strcmp(argv[i], "--host") == 0) {
if (i + 1 >= argc) {
std::cerr << "Error: --host requires a hostname argument" << std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.hostname = argv[++i];
} else if (strcmp(argv[i], "--port") == 0) {
if (i + 1 >= argc) {
std::cerr << "Error: --port requires a port number argument"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.port = std::atoi(argv[++i]);
if (config.port <= 0 || config.port > 65535) {
std::cerr << "Error: Invalid port number. Must be between 1 and 65535"
<< std::endl;
return ParseResult::ERROR;
}
} else if (strcmp(argv[i], "--mount") == 0) {
if (i + 1 >= argc) {
std::cerr
<< "Error: --mount requires mount_point:document_root argument"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
std::string mount_arg = argv[++i];
auto colon_pos = mount_arg.find(':');
if (colon_pos == std::string::npos) {
std::cerr << "Error: --mount argument must be in format "
"mount_point:document_root"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.mount_point = mount_arg.substr(0, colon_pos);
config.document_root = mount_arg.substr(colon_pos + 1);
if (config.mount_point.empty() || config.document_root.empty()) {
std::cerr
<< "Error: Both mount_point and document_root must be non-empty"
<< std::endl;
return ParseResult::ERROR;
}
} 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]);
return ParseResult::ERROR;
}
}
return ParseResult::SUCCESS;
}
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
<< std::format(
"Error: Cannot mount '{}' to '{}'. Directory may not exist.",
config.mount_point, config.document_root)
<< std::endl;
return false;
}
svr.set_file_extension_and_mimetype_mapping("html", "text/html");
svr.set_file_extension_and_mimetype_mapping("htm", "text/html");
svr.set_file_extension_and_mimetype_mapping("css", "text/css");
svr.set_file_extension_and_mimetype_mapping("js", "text/javascript");
svr.set_file_extension_and_mimetype_mapping("json", "application/json");
svr.set_file_extension_and_mimetype_mapping("xml", "application/xml");
svr.set_file_extension_and_mimetype_mapping("png", "image/png");
svr.set_file_extension_and_mimetype_mapping("jpg", "image/jpeg");
svr.set_file_extension_and_mimetype_mapping("jpeg", "image/jpeg");
svr.set_file_extension_and_mimetype_mapping("gif", "image/gif");
svr.set_file_extension_and_mimetype_mapping("svg", "image/svg+xml");
svr.set_file_extension_and_mimetype_mapping("ico", "image/x-icon");
svr.set_file_extension_and_mimetype_mapping("pdf", "application/pdf");
svr.set_file_extension_and_mimetype_mapping("zip", "application/zip");
svr.set_file_extension_and_mimetype_mapping("txt", "text/plain");
svr.set_error_handler([](const Request & /*req*/, Response &res) {
if (res.status == 404) {
res.set_content(
std::format(
"<html><head><title>404 Not Found</title></head>"
"<body><h1>404 Not Found</h1>"
"<p>The requested resource was not found on this server.</p>"
"<hr><p>{}</p></body></html>",
SERVER_NAME),
"text/html");
}
});
svr.set_logger(
[](auto &req, auto &res) { std::cout << log(req, res) << std::endl; });
svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) {
res.set_header("Server", SERVER_NAME);
return Server::HandlerResponse::Unhandled;
});
svr.set_mount_point("/", base_dir);
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
std::cout << std::format("Serving HTTP on {0} port {1} ...", host, port)
return true;
}
int main(int argc, char *argv[]) {
ServerConfig config;
auto result = parse_command_line(argc, argv, config);
switch (result) {
case ParseResult::HELP_REQUESTED:
case ParseResult::VERSION_REQUESTED: return 0;
case ParseResult::ERROR: return 1;
case ParseResult::SUCCESS: break;
}
if (!setup_server(svr, config)) { return 1; }
std::cout << "Serving HTTP on " << config.hostname << ":" << config.port
<< std::endl;
std::cout << "Mount point: " << config.mount_point << " -> "
<< config.document_root << std::endl;
auto ret = svr.listen(host, port);
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);
std::cout << "Server has been shut down." << std::endl;
return ret ? 0 : 1;
}
+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);
});
});
+518 -149
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -39,12 +39,12 @@ endif
deps = [dependency('threads')]
args = []
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('cpp-httplib_openssl'))
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('openssl'))
if openssl_dep.found()
deps += openssl_dep
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
if host_machine.system() == 'darwin'
macosx_keychain_dep = dependency('appleframeworks', modules: ['CoreFoundation', 'Security'], required: get_option('cpp-httplib_macosx_keychain'))
macosx_keychain_dep = dependency('appleframeworks', modules: ['CoreFoundation', 'Security'], required: get_option('macosx_keychain'))
if macosx_keychain_dep.found()
deps += macosx_keychain_dep
args += '-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN'
@@ -52,15 +52,15 @@ if openssl_dep.found()
endif
endif
zlib_dep = dependency('zlib', required: get_option('cpp-httplib_zlib'))
zlib_dep = dependency('zlib', required: get_option('zlib'))
if zlib_dep.found()
deps += zlib_dep
args += '-DCPPHTTPLIB_ZLIB_SUPPORT'
endif
brotli_deps = [dependency('libbrotlicommon', required: get_option('cpp-httplib_brotli'))]
brotli_deps += dependency('libbrotlidec', required: get_option('cpp-httplib_brotli'))
brotli_deps += dependency('libbrotlienc', required: get_option('cpp-httplib_brotli'))
brotli_deps = [dependency('libbrotlicommon', required: get_option('brotli'))]
brotli_deps += dependency('libbrotlidec', required: get_option('brotli'))
brotli_deps += dependency('libbrotlienc', required: get_option('brotli'))
brotli_found_all = true
foreach brotli_dep : brotli_deps
@@ -74,7 +74,7 @@ if brotli_found_all
args += '-DCPPHTTPLIB_BROTLI_SUPPORT'
endif
async_ns_opt = get_option('cpp-httplib_non_blocking_getaddrinfo')
async_ns_opt = get_option('non_blocking_getaddrinfo')
if host_machine.system() == 'windows'
async_ns_dep = cxx.find_library('ws2_32', required: async_ns_opt)
@@ -91,7 +91,7 @@ endif
cpp_httplib_dep = dependency('', required: false)
if get_option('cpp-httplib_compile')
if get_option('compile')
python3 = find_program('python3')
httplib_ch = custom_target(
@@ -135,6 +135,6 @@ endif
meson.override_dependency('cpp-httplib', cpp_httplib_dep)
if get_option('cpp-httplib_test')
if get_option('test')
subdir('test')
endif
+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
+783 -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");
})
@@ -3200,6 +3289,11 @@ protected:
[&](const Request & /*req*/, Response &res) {
res.set_content("abcdefg", "text/plain");
})
.Get("/test-start-time",
[&](const Request &req, Response & /*res*/) {
EXPECT_NE(req.start_time_,
std::chrono::steady_clock::time_point::min());
})
.Get("/with-range-customized-response",
[&](const Request & /*req*/, Response &res) {
res.status = StatusCode::BadRequest_400;
@@ -3607,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);
@@ -5800,6 +5854,8 @@ TEST_F(ServerTest, TooManyRedirect) {
EXPECT_EQ(Error::ExceedRedirectCount, res.error());
}
TEST_F(ServerTest, StartTime) { auto res = cli_.Get("/test-start-time"); }
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
TEST_F(ServerTest, Gzip) {
Headers headers;
@@ -7359,6 +7415,48 @@ TEST(KeepAliveTest, SSLClientReconnectionPost) {
ASSERT_TRUE(result);
EXPECT_EQ(200, result->status);
}
TEST(SNI_AutoDetectionTest, SNI_Logic) {
{
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
svr.Get("/sni", [&](const Request &req, Response &res) {
std::string expected;
if (req.ssl) {
if (const char *sni =
SSL_get_servername(req.ssl, TLSEXT_NAMETYPE_host_name)) {
expected = sni;
}
}
EXPECT_EQ(expected, req.get_param_value("expected"));
res.set_content("ok", "text/plain");
});
auto listen_thread = std::thread([&svr] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
{
SSLClient cli("localhost", PORT);
cli.enable_server_certificate_verification(false);
auto res = cli.Get("/sni?expected=localhost");
ASSERT_TRUE(res);
}
{
SSLClient cli("::1", PORT);
cli.enable_server_certificate_verification(false);
auto res = cli.Get("/sni?expected=");
ASSERT_TRUE(res);
}
}
}
#endif
TEST(ClientProblemDetectionTest, ContentProvider) {
@@ -8296,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");
@@ -8638,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) {
@@ -8963,6 +9307,46 @@ TEST(HttpToHttpsRedirectTest, CertFile) {
ASSERT_EQ(StatusCode::OK_200, res->status);
}
TEST(SSLClientRedirectTest, CertFile) {
SSLServer ssl_svr1(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(ssl_svr1.is_valid());
ssl_svr1.Get("/index", [&](const Request &, Response &res) {
res.set_redirect("https://127.0.0.1:1235/index");
ssl_svr1.stop();
});
SSLServer ssl_svr2(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(ssl_svr2.is_valid());
ssl_svr2.Get("/index", [&](const Request &, Response &res) {
res.set_content("test", "text/plain");
ssl_svr2.stop();
});
thread t = thread([&]() { ASSERT_TRUE(ssl_svr1.listen("127.0.0.1", PORT)); });
thread t2 =
thread([&]() { ASSERT_TRUE(ssl_svr2.listen("127.0.0.1", 1235)); });
auto se = detail::scope_exit([&] {
t2.join();
t.join();
ASSERT_FALSE(ssl_svr1.is_running());
});
ssl_svr1.wait_until_ready();
ssl_svr2.wait_until_ready();
SSLClient cli("127.0.0.1", PORT);
std::string cert;
read_file(SERVER_CERT2_FILE, cert);
cli.load_ca_cert_store(cert.c_str(), cert.size());
cli.enable_server_certificate_verification(true);
cli.set_follow_location(true);
cli.set_connection_timeout(30);
auto res = cli.Get("/index");
ASSERT_TRUE(res);
ASSERT_EQ(StatusCode::OK_200, res->status);
}
TEST(MultipartFormDataTest, LargeData) {
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
@@ -10209,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;
@@ -10589,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) {
@@ -10618,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);
});
});
@@ -10662,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();
}
}
@@ -10718,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");
}