mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eacc1ca98e | |||
| ac9ebb0ee3 | |||
| 11eed05ce7 | |||
| f3bba0646a | |||
| 2da189f88c | |||
| 6e0f211cff | |||
| 318a3fe425 | |||
| 2d8d524178 | |||
| afa88dbe70 | |||
| 08133b593b | |||
| 8aedbf4547 | |||
| cde29362ef | |||
| bae40fcdf2 | |||
| db561f5552 | |||
| 35c52c1ab9 | |||
| 23ff9a5605 | |||
| 41be1e24e3 | |||
| 6e52d0a057 | |||
| f72b4582e6 | |||
| 89c932f313 | |||
| eb5a65e0df | |||
| 7a3b92bbd9 | |||
| eb11032797 | |||
| 54e75dc8ef | |||
| b20b5fdd1f | |||
| f4cc542d4b | |||
| 4285d33992 | |||
| 92b4f53012 | |||
| b8e21eac89 | |||
| 3fae5f1473 | |||
| fe7fe15d2e | |||
| fbd6ce7a3f | |||
| dffce89514 | |||
| 3f44c80fd3 | |||
| a2bb6f6c1e | |||
| 7012e765e1 | |||
| b1c1fa2dc6 | |||
| fbee136dca | |||
| 70cca55caf | |||
| cdaed14925 | |||
| b52d7d8411 | |||
| acf28a362d | |||
| 0b3758ec36 |
@@ -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
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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++
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+9
-9
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user