mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f2717e623 | |||
| 257b266190 | |||
| ba0d0b82db | |||
| 5ecba74a99 | |||
| ec1ffbc27d | |||
| 4978f26f86 | |||
| bb7c7ab075 | |||
| 1c3d35f83c | |||
| b1bb2b7ecc | |||
| f6ed5fc60f | |||
| 69d468f4d9 | |||
| 2e61fd3e6e | |||
| 3ad4a4243a | |||
| 511e3ef9e5 | |||
| f787f31b87 | |||
| 43a54a3e3d | |||
| 83e98a28dd | |||
| 4d7c9a788d | |||
| 1cd0347ace | |||
| b3a8af80b9 | |||
| 1e97c28e36 | |||
| d279eff4db | |||
| 188035fb6d | |||
| 125272f34b | |||
| 9ced2f614d |
@@ -228,7 +228,7 @@ jobs:
|
||||
for ($i = 0; $i -lt $shards; $i++) {
|
||||
$log = "shard_${i}.log"
|
||||
$procs += Start-Process -FilePath ./Release/httplib-test.exe `
|
||||
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}-*BenchmarkTest*" `
|
||||
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}" `
|
||||
-NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
|
||||
-Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
|
||||
}
|
||||
@@ -248,9 +248,6 @@ jobs:
|
||||
}
|
||||
if ($failed) { exit 1 }
|
||||
Write-Host "All shards passed."
|
||||
- name: Run benchmark tests with retry ${{ matrix.config.name }}
|
||||
if: ${{ matrix.config.run_tests }}
|
||||
run: ctest --output-on-failure --test-dir build -C Release -R "BenchmarkTest" --repeat until-pass:5
|
||||
|
||||
env:
|
||||
VCPKG_ROOT: "C:/vcpkg"
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
name: benchmark
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ubuntu:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: build and run
|
||||
run: cd test && make test_benchmark && ./test_benchmark
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: build and run
|
||||
run: cd test && make test_benchmark && ./test_benchmark
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
steps:
|
||||
- name: Prepare Git for Checkout on Windows
|
||||
run: |
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
- name: Cache vcpkg packages
|
||||
id: vcpkg-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: C:/vcpkg/installed
|
||||
key: vcpkg-installed-windows-gtest
|
||||
- name: Install vcpkg dependencies
|
||||
if: steps.vcpkg-cache.outputs.cache-hit != 'true'
|
||||
run: vcpkg install gtest
|
||||
- name: Configure and build
|
||||
shell: pwsh
|
||||
run: |
|
||||
$cmake_content = @"
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(httplib-benchmark CXX)
|
||||
find_package(GTest REQUIRED)
|
||||
add_executable(httplib-benchmark test/test_benchmark.cc)
|
||||
target_include_directories(httplib-benchmark PRIVATE .)
|
||||
target_link_libraries(httplib-benchmark PRIVATE GTest::gtest_main)
|
||||
target_compile_options(httplib-benchmark PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
|
||||
"@
|
||||
New-Item -ItemType Directory -Force -Path build_bench/test | Out-Null
|
||||
Set-Content -Path build_bench/CMakeLists.txt -Value $cmake_content
|
||||
Copy-Item -Path httplib.h -Destination build_bench/
|
||||
Copy-Item -Path test/test_benchmark.cc -Destination build_bench/test/
|
||||
cmake -B build_bench/build -S build_bench `
|
||||
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
|
||||
cmake --build build_bench/build --config Release
|
||||
- name: Run with retry
|
||||
run: ctest --output-on-failure --test-dir build_bench/build -C Release --repeat until-pass:5
|
||||
env:
|
||||
VCPKG_ROOT: "C:/vcpkg"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
@@ -28,6 +28,10 @@ jobs:
|
||||
- name: Run proxy tests (OpenSSL)
|
||||
if: matrix.tls_backend == 'openssl'
|
||||
run: cd test && make proxy
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
|
||||
- name: Run proxy tests (Mbed TLS)
|
||||
if: matrix.tls_backend == 'mbedtls'
|
||||
run: cd test && make proxy_mbedtls
|
||||
run: cd test && make proxy_mbedtls
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
|
||||
@@ -1,4 +1,7 @@
|
||||
tags
|
||||
AGENTS.md
|
||||
docs-src/pages/AGENTS.md
|
||||
plans/
|
||||
|
||||
# Ignore executables (no extension) but not source files
|
||||
example/server
|
||||
@@ -48,6 +51,7 @@ test/test_split_wolfssl
|
||||
test/test_split_no_tls
|
||||
test/test_websocket_heartbeat
|
||||
test/test_thread_pool
|
||||
test/test_benchmark
|
||||
test/test.xcodeproj/xcuser*
|
||||
test/test.xcodeproj/*/xcuser*
|
||||
test/*.o
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@ cpp-httplib provides multiple API layers for different use cases:
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SSEClient (planned) │ ← SSE-specific, parsed events
|
||||
│ SSEClient │ ← SSE-specific, parsed events
|
||||
│ - on_message(), on_event() │
|
||||
│ - Auto-reconnect, Last-Event-ID │
|
||||
├─────────────────────────────────────────────┤
|
||||
@@ -61,7 +61,7 @@ cpp-httplib provides multiple API layers for different use cases:
|
||||
|
||||
| Use Case | Recommended API |
|
||||
|----------|----------------|
|
||||
| SSE with auto-reconnect | SSEClient (planned) or `ssecli-stream.cc` example |
|
||||
| SSE with auto-reconnect | `SSEClient` (see [README-sse.md](README-sse.md)) |
|
||||
| LLM streaming (JSON Lines) | `stream::Get()` |
|
||||
| Large file download | `stream::Get()` or `open_stream()` |
|
||||
| Reverse proxy | `open_stream()` |
|
||||
|
||||
@@ -353,6 +353,26 @@ if (ws.connect()) {
|
||||
| `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5` | Timeout for waiting peer's Close response (seconds) |
|
||||
| `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30` | Automatic Ping interval for heartbeat (seconds) |
|
||||
|
||||
### Runtime Ping Interval
|
||||
|
||||
You can override the ping interval at runtime instead of changing the compile-time macro. Set it to `0` to disable automatic pings entirely.
|
||||
|
||||
```cpp
|
||||
// Server side
|
||||
httplib::Server svr;
|
||||
svr.set_websocket_ping_interval(10); // 10 seconds
|
||||
|
||||
// Or using std::chrono
|
||||
svr.set_websocket_ping_interval(std::chrono::seconds(10));
|
||||
|
||||
// Client side
|
||||
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
|
||||
ws.set_websocket_ping_interval(10); // 10 seconds
|
||||
|
||||
// Disable automatic pings
|
||||
ws.set_websocket_ping_interval(0);
|
||||
```
|
||||
|
||||
## Threading Model
|
||||
|
||||
WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime.
|
||||
|
||||
@@ -64,26 +64,14 @@ if (auto res = cli.Get("/hi")) {
|
||||
|
||||
cpp-httplib supports multiple TLS backends through an abstraction layer:
|
||||
|
||||
| Backend | Define | Libraries |
|
||||
| :------ | :----- | :-------- |
|
||||
| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` |
|
||||
| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` |
|
||||
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` |
|
||||
| Backend | Define | Libraries | Notes |
|
||||
| :------ | :----- | :-------- | :---- |
|
||||
| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` | [3.0 or later](https://www.openssl.org/policies/releasestrat.html) required |
|
||||
| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` | 2.x and 3.x supported (auto-detected) |
|
||||
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` | 5.x supported; must build with `--enable-opensslall` |
|
||||
|
||||
> [!NOTE]
|
||||
> OpenSSL 3.0 or later is required. Please see [this page](https://www.openssl.org/policies/releasestrat.html) for more information.
|
||||
|
||||
> [!NOTE]
|
||||
> Mbed TLS 2.x and 3.x are supported. The library automatically detects the version and uses the appropriate API.
|
||||
|
||||
> [!NOTE]
|
||||
> wolfSSL must be built with OpenSSL compatibility layer enabled (`--enable-opensslall`). wolfSSL 5.x is supported.
|
||||
|
||||
> [!NOTE]
|
||||
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()` or `load_ca_cert_store(pem, size)`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable with these backends.
|
||||
|
||||
> [!TIP]
|
||||
> For macOS: cpp-httplib automatically loads system certs from the Keychain when a TLS backend is enabled. `CoreFoundation` and `Security` must be linked with `-framework`. To disable this, define `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`.
|
||||
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable.
|
||||
|
||||
```c++
|
||||
// Use either OpenSSL, Mbed TLS, or wolfSSL
|
||||
@@ -197,27 +185,21 @@ svr.Get("/", [](const httplib::Request &req, httplib::Response &res) {
|
||||
});
|
||||
```
|
||||
|
||||
### Windows Certificate Verification
|
||||
### Platform-specific Certificate Handling
|
||||
|
||||
On Windows, cpp-httplib automatically performs additional certificate verification using the Windows certificate store via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`). This works with all TLS backends (OpenSSL, Mbed TLS, and wolfSSL), providing:
|
||||
cpp-httplib automatically integrates with the OS certificate store on macOS and Windows. This works with all TLS backends.
|
||||
|
||||
- Real-time certificate validation integrated with Windows Update
|
||||
- Certificate revocation checking
|
||||
- SSL/TLS policy verification using the system certificate store (ROOT and CA)
|
||||
| Platform | Behavior | Disable (compile time) |
|
||||
| :------- | :------- | :--------------------- |
|
||||
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`) | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
|
||||
| Windows | Verifies certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
||||
|
||||
This feature is enabled by default and can be controlled at runtime:
|
||||
On Windows, verification can also be disabled at runtime:
|
||||
|
||||
```c++
|
||||
// Disable Windows certificate verification (use only OpenSSL/Mbed TLS/wolfSSL verification)
|
||||
cli.enable_windows_certificate_verification(false);
|
||||
```
|
||||
|
||||
To disable this feature at compile time, define:
|
||||
|
||||
```c++
|
||||
#define CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> When using SSL, it seems impossible to avoid SIGPIPE in all cases, since on some operating systems, SIGPIPE can only be suppressed on a per-message basis, but there is no way to make the OpenSSL library do so for its internal communications. If your program needs to avoid being terminated on SIGPIPE, the only fully general way might be to set up a signal handler for SIGPIPE to handle or ignore it yourself.
|
||||
|
||||
@@ -350,6 +332,11 @@ The following are built-in mappings:
|
||||
> [!WARNING]
|
||||
> These static file server methods are not thread-safe.
|
||||
|
||||
<!-- -->
|
||||
|
||||
> [!NOTE]
|
||||
> On POSIX systems, the static file server rejects requests that resolve (via symlinks) to a path outside the mounted base directory. Ensure that the served directory has appropriate permissions, as managing access to the served directory is the application developer's responsibility.
|
||||
|
||||
### File request handler
|
||||
|
||||
```cpp
|
||||
@@ -541,16 +528,16 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
|
||||
}
|
||||
|
||||
// IMPORTANT: file.filename is an untrusted value from the client.
|
||||
// Always extract only the basename to prevent path traversal attacks.
|
||||
auto safe_name = std::filesystem::path(file.filename).filename();
|
||||
if (safe_name.empty() || safe_name == "." || safe_name == "..") {
|
||||
// Always sanitize to prevent path traversal attacks.
|
||||
auto safe_name = httplib::sanitize_filename(file.filename);
|
||||
if (safe_name.empty()) {
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
res.set_content("Invalid filename", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to disk
|
||||
std::ofstream ofs(upload_dir / safe_name, std::ios::binary);
|
||||
std::ofstream ofs(upload_dir + "/" + safe_name, std::ios::binary);
|
||||
ofs << file.content;
|
||||
}
|
||||
|
||||
@@ -586,6 +573,16 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
|
||||
});
|
||||
```
|
||||
|
||||
#### Filename Sanitization
|
||||
|
||||
`file.filename` in multipart uploads is an untrusted value from the client. Always sanitize before using it in file paths:
|
||||
|
||||
```cpp
|
||||
auto safe = httplib::sanitize_filename(file.filename);
|
||||
```
|
||||
|
||||
This function strips path separators (`/`, `\`), null bytes, leading/trailing whitespace, and rejects `.` and `..`. Returns an empty string if the filename is unsafe.
|
||||
|
||||
### Receive content with a content receiver
|
||||
|
||||
```cpp
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.37.1"
|
||||
version = "0.38.0"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -24,7 +24,7 @@ A collection of recipes that answer "How do I...?" questions. Each recipe is sel
|
||||
|
||||
### Streaming & Progress
|
||||
- Receive a response as a stream
|
||||
- Use the progress callback (`set_progress`)
|
||||
- Use the progress callback (`DownloadProgress` / `UploadProgress`)
|
||||
|
||||
### Connection & Performance
|
||||
- Set timeouts (`set_connection_timeout` / `set_read_timeout`)
|
||||
@@ -90,6 +90,6 @@ A collection of recipes that answer "How do I...?" questions. Each recipe is sel
|
||||
## WebSocket
|
||||
|
||||
- Implement a WebSocket echo server and client
|
||||
- Configure heartbeats (`set_websocket_ping_interval`)
|
||||
- Configure heartbeats (`set_websocket_ping_interval` / `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`)
|
||||
- Handle connection close
|
||||
- Send and receive binary frames
|
||||
|
||||
@@ -60,7 +60,7 @@ For uploading large files, `make_file_provider()` comes in handy. It streams the
|
||||
```cpp
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
|
||||
auto res = cli.Post("/upload", {}, {
|
||||
auto res = cli.Post("/upload", {}, {}, {
|
||||
httplib::make_file_provider("file", "/path/to/large-file.zip")
|
||||
});
|
||||
```
|
||||
@@ -160,16 +160,16 @@ svr.set_post_routing_handler([](const auto &req, auto &res) {
|
||||
});
|
||||
```
|
||||
|
||||
Use `req.user_data` to pass data from middleware to handlers. This is useful for sharing things like decoded auth tokens.
|
||||
Use `res.user_data` to pass data from middleware to handlers. This is useful for sharing things like decoded auth tokens.
|
||||
|
||||
```cpp
|
||||
svr.set_pre_routing_handler([](const auto &req, auto &res) {
|
||||
req.user_data["auth_user"] = std::string("alice");
|
||||
res.user_data["auth_user"] = std::string("alice");
|
||||
return httplib::Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/me", [](const auto &req, auto &res) {
|
||||
auto user = std::any_cast<std::string>(req.user_data.at("auth_user"));
|
||||
auto user = std::any_cast<std::string>(res.user_data.at("auth_user"));
|
||||
res.set_content("Hello, " + user, "text/plain");
|
||||
});
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ order: 0
|
||||
|
||||
### ストリーミング・進捗
|
||||
- レスポンスをストリーミングで受信する
|
||||
- 進捗コールバックを使う(`set_progress`)
|
||||
- 進捗コールバックを使う(`DownloadProgress` / `UploadProgress`)
|
||||
|
||||
### 接続・パフォーマンス
|
||||
- タイムアウトを設定する(`set_connection_timeout` / `set_read_timeout`)
|
||||
@@ -90,6 +90,6 @@ order: 0
|
||||
## WebSocket
|
||||
|
||||
- WebSocket エコーサーバー/クライアントを実装する
|
||||
- ハートビートを設定する(`set_websocket_ping_interval`)
|
||||
- ハートビートを設定する(`set_websocket_ping_interval` / `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`)
|
||||
- 接続クローズをハンドリングする
|
||||
- バイナリフレームを送受信する
|
||||
|
||||
@@ -60,7 +60,7 @@ svr.Get("/stream", [](const auto &, auto &res) {
|
||||
```cpp
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
|
||||
auto res = cli.Post("/upload", {}, {
|
||||
auto res = cli.Post("/upload", {}, {}, {
|
||||
httplib::make_file_provider("file", "/path/to/large-file.zip")
|
||||
});
|
||||
```
|
||||
@@ -160,16 +160,16 @@ svr.set_post_routing_handler([](const auto &req, auto &res) {
|
||||
});
|
||||
```
|
||||
|
||||
`req.user_data` を使うと、ミドルウェアからハンドラーにデータを渡せます。認証トークンのデコード結果を共有するときに便利です。
|
||||
`res.user_data` を使うと、ミドルウェアからハンドラーにデータを渡せます。認証トークンのデコード結果を共有するときに便利です。
|
||||
|
||||
```cpp
|
||||
svr.set_pre_routing_handler([](const auto &req, auto &res) {
|
||||
req.user_data["auth_user"] = std::string("alice");
|
||||
res.user_data["auth_user"] = std::string("alice");
|
||||
return httplib::Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/me", [](const auto &req, auto &res) {
|
||||
auto user = std::any_cast<std::string>(req.user_data.at("auth_user"));
|
||||
auto user = std::any_cast<std::string>(res.user_data.at("auth_user"));
|
||||
res.set_content("Hello, " + user, "text/plain");
|
||||
});
|
||||
```
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.37.1"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002501"
|
||||
#define CPPHTTPLIB_VERSION "0.38.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002600"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -689,6 +689,18 @@ inline from_chars_result<double> from_chars(const char *first, const char *last,
|
||||
return {first + (endptr - s.c_str()), std::errc{}};
|
||||
}
|
||||
|
||||
inline bool parse_port(const char *s, size_t len, int &port) {
|
||||
int val = 0;
|
||||
auto r = from_chars(s, s + len, val);
|
||||
if (r.ec != std::errc{} || val < 1 || val > 65535) { return false; }
|
||||
port = val;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool parse_port(const std::string &s, int &port) {
|
||||
return parse_port(s.data(), s.size(), port);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
enum SSLVerifierResponse {
|
||||
@@ -1658,6 +1670,11 @@ public:
|
||||
|
||||
Server &set_payload_max_length(size_t length);
|
||||
|
||||
Server &set_websocket_ping_interval(time_t sec);
|
||||
template <class Rep, class Period>
|
||||
Server &set_websocket_ping_interval(
|
||||
const std::chrono::duration<Rep, Period> &duration);
|
||||
|
||||
bool bind_to_port(const std::string &host, int port, int socket_flags = 0);
|
||||
int bind_to_any_port(const std::string &host, int socket_flags = 0);
|
||||
bool listen_after_bind();
|
||||
@@ -1692,6 +1709,8 @@ protected:
|
||||
time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
|
||||
time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
|
||||
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
|
||||
time_t websocket_ping_interval_sec_ =
|
||||
CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
|
||||
|
||||
private:
|
||||
using Handlers =
|
||||
@@ -1761,6 +1780,7 @@ private:
|
||||
struct MountPointEntry {
|
||||
std::string mount_point;
|
||||
std::string base_dir;
|
||||
std::string resolved_base_dir;
|
||||
Headers headers;
|
||||
};
|
||||
std::vector<MountPointEntry> base_dirs_;
|
||||
@@ -2178,6 +2198,10 @@ protected:
|
||||
|
||||
virtual bool create_and_connect_socket(Socket &socket, Error &error);
|
||||
virtual bool ensure_socket_connection(Socket &socket, Error &error);
|
||||
virtual bool setup_proxy_connection(
|
||||
Socket &socket,
|
||||
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
||||
Response &res, bool &success, Error &error);
|
||||
|
||||
// All of:
|
||||
// shutdown_ssl
|
||||
@@ -2704,6 +2728,10 @@ private:
|
||||
std::function<bool(Stream &strm)> callback) override;
|
||||
bool is_ssl() const override;
|
||||
|
||||
bool setup_proxy_connection(
|
||||
Socket &socket,
|
||||
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
||||
Response &res, bool &success, Error &error) override;
|
||||
bool connect_with_proxy(
|
||||
Socket &sock,
|
||||
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
||||
@@ -2903,6 +2931,8 @@ std::string encode_query_component(const std::string &component,
|
||||
std::string decode_query_component(const std::string &component,
|
||||
bool plus_as_space = true);
|
||||
|
||||
std::string sanitize_filename(const std::string &filename);
|
||||
|
||||
std::string append_query_params(const std::string &path, const Params ¶ms);
|
||||
|
||||
std::pair<std::string, std::string> make_range_header(const Ranges &ranges);
|
||||
@@ -3706,15 +3736,19 @@ private:
|
||||
friend class httplib::Server;
|
||||
friend class WebSocketClient;
|
||||
|
||||
WebSocket(Stream &strm, const Request &req, bool is_server)
|
||||
: strm_(strm), req_(req), is_server_(is_server) {
|
||||
WebSocket(
|
||||
Stream &strm, const Request &req, bool is_server,
|
||||
time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND)
|
||||
: strm_(strm), req_(req), is_server_(is_server),
|
||||
ping_interval_sec_(ping_interval_sec) {
|
||||
start_heartbeat();
|
||||
}
|
||||
|
||||
WebSocket(std::unique_ptr<Stream> &&owned_strm, const Request &req,
|
||||
bool is_server)
|
||||
WebSocket(
|
||||
std::unique_ptr<Stream> &&owned_strm, const Request &req, bool is_server,
|
||||
time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND)
|
||||
: strm_(*owned_strm), owned_strm_(std::move(owned_strm)), req_(req),
|
||||
is_server_(is_server) {
|
||||
is_server_(is_server), ping_interval_sec_(ping_interval_sec) {
|
||||
start_heartbeat();
|
||||
}
|
||||
|
||||
@@ -3725,6 +3759,7 @@ private:
|
||||
std::unique_ptr<Stream> owned_strm_;
|
||||
Request req_;
|
||||
bool is_server_;
|
||||
time_t ping_interval_sec_;
|
||||
std::atomic<bool> closed_{false};
|
||||
std::mutex write_mutex_;
|
||||
std::thread ping_thread_;
|
||||
@@ -3753,6 +3788,7 @@ public:
|
||||
const std::string &subprotocol() const;
|
||||
void set_read_timeout(time_t sec, time_t usec = 0);
|
||||
void set_write_timeout(time_t sec, time_t usec = 0);
|
||||
void set_websocket_ping_interval(time_t sec);
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
void set_ca_cert_path(const std::string &path);
|
||||
@@ -3776,6 +3812,8 @@ private:
|
||||
time_t read_timeout_usec_ = 0;
|
||||
time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;
|
||||
time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
|
||||
time_t websocket_ping_interval_sec_ =
|
||||
CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
bool is_ssl_ = false;
|
||||
@@ -4822,6 +4860,30 @@ inline bool is_valid_path(const std::string &path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool canonicalize_path(const char *path, std::string &resolved) {
|
||||
#if defined(_WIN32)
|
||||
char buf[_MAX_PATH];
|
||||
if (_fullpath(buf, path, _MAX_PATH) == nullptr) { return false; }
|
||||
resolved = buf;
|
||||
#else
|
||||
char buf[PATH_MAX];
|
||||
if (realpath(path, buf) == nullptr) { return false; }
|
||||
resolved = buf;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool is_path_within_base(const std::string &resolved_path,
|
||||
const std::string &resolved_base) {
|
||||
#if defined(_WIN32)
|
||||
return _strnicmp(resolved_path.c_str(), resolved_base.c_str(),
|
||||
resolved_base.size()) == 0;
|
||||
#else
|
||||
return strncmp(resolved_path.c_str(), resolved_base.c_str(),
|
||||
resolved_base.size()) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline FileStat::FileStat(const std::string &path) {
|
||||
#if defined(_WIN32)
|
||||
auto wpath = u8string_to_wstring(path.c_str());
|
||||
@@ -5792,9 +5854,9 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len);
|
||||
|
||||
// Set port if service is specified
|
||||
if (service && strlen(service) > 0) {
|
||||
int port = atoi(service);
|
||||
if (port > 0) {
|
||||
if (service && *service) {
|
||||
int port = 0;
|
||||
if (parse_port(service, strlen(service), port)) {
|
||||
if (sockaddr_ptr->sa_family == AF_INET) {
|
||||
reinterpret_cast<struct sockaddr_in *>((*current)->ai_addr)
|
||||
->sin_port = htons(static_cast<uint16_t>(port));
|
||||
@@ -6424,33 +6486,114 @@ inline bool can_compress_content_type(const std::string &content_type) {
|
||||
}
|
||||
}
|
||||
|
||||
inline bool parse_quality(const char *b, const char *e, std::string &token,
|
||||
double &quality) {
|
||||
quality = 1.0;
|
||||
token.clear();
|
||||
|
||||
// Split on first ';': left = token name, right = parameters
|
||||
const char *params_b = nullptr;
|
||||
std::size_t params_len = 0;
|
||||
|
||||
divide(
|
||||
b, static_cast<std::size_t>(e - b), ';',
|
||||
[&](const char *lb, std::size_t llen, const char *rb, std::size_t rlen) {
|
||||
auto r = trim(lb, lb + llen, 0, llen);
|
||||
if (r.first < r.second) { token.assign(lb + r.first, lb + r.second); }
|
||||
params_b = rb;
|
||||
params_len = rlen;
|
||||
});
|
||||
|
||||
if (token.empty()) { return false; }
|
||||
if (params_len == 0) { return true; }
|
||||
|
||||
// Scan parameters for q= (stops on first match)
|
||||
bool invalid = false;
|
||||
split_find(params_b, params_b + params_len, ';',
|
||||
(std::numeric_limits<size_t>::max)(),
|
||||
[&](const char *pb, const char *pe) -> bool {
|
||||
// Match exactly "q=" or "Q=" (not "query=" etc.)
|
||||
auto len = static_cast<size_t>(pe - pb);
|
||||
if (len < 2) { return false; }
|
||||
if ((pb[0] != 'q' && pb[0] != 'Q') || pb[1] != '=') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trim the value portion
|
||||
auto r = trim(pb, pe, 2, len);
|
||||
if (r.first >= r.second) {
|
||||
invalid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
double v = 0.0;
|
||||
auto res = from_chars(pb + r.first, pb + r.second, v);
|
||||
if (res.ec != std::errc{} || v < 0.0 || v > 1.0) {
|
||||
invalid = true;
|
||||
return true;
|
||||
}
|
||||
quality = v;
|
||||
return true;
|
||||
});
|
||||
|
||||
return !invalid;
|
||||
}
|
||||
|
||||
inline EncodingType encoding_type(const Request &req, const Response &res) {
|
||||
auto ret =
|
||||
detail::can_compress_content_type(res.get_header_value("Content-Type"));
|
||||
if (!ret) { return EncodingType::None; }
|
||||
if (!can_compress_content_type(res.get_header_value("Content-Type"))) {
|
||||
return EncodingType::None;
|
||||
}
|
||||
|
||||
const auto &s = req.get_header_value("Accept-Encoding");
|
||||
(void)(s);
|
||||
if (s.empty()) { return EncodingType::None; }
|
||||
|
||||
// Single-pass: iterate tokens and track the best supported encoding.
|
||||
// Server preference breaks ties (br > gzip > zstd).
|
||||
EncodingType best = EncodingType::None;
|
||||
double best_q = 0.0; // q=0 means "not acceptable"
|
||||
|
||||
// Server preference: Brotli > Gzip > Zstd (lower = more preferred)
|
||||
auto priority = [](EncodingType t) -> int {
|
||||
switch (t) {
|
||||
case EncodingType::Brotli: return 0;
|
||||
case EncodingType::Gzip: return 1;
|
||||
case EncodingType::Zstd: return 2;
|
||||
default: return 3;
|
||||
}
|
||||
};
|
||||
|
||||
std::string name;
|
||||
split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) {
|
||||
double quality = 1.0;
|
||||
if (!parse_quality(b, e, name, quality)) { return; }
|
||||
if (quality <= 0.0) { return; }
|
||||
|
||||
EncodingType type = EncodingType::None;
|
||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||
// TODO: 'Accept-Encoding' has br, not br;q=0
|
||||
ret = s.find("br") != std::string::npos;
|
||||
if (ret) { return EncodingType::Brotli; }
|
||||
if (case_ignore::equal(name, "br")) { type = EncodingType::Brotli; }
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
// TODO: 'Accept-Encoding' has gzip, not gzip;q=0
|
||||
ret = s.find("gzip") != std::string::npos;
|
||||
if (ret) { return EncodingType::Gzip; }
|
||||
if (type == EncodingType::None && case_ignore::equal(name, "gzip")) {
|
||||
type = EncodingType::Gzip;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_ZSTD_SUPPORT
|
||||
// TODO: 'Accept-Encoding' has zstd, not zstd;q=0
|
||||
ret = s.find("zstd") != std::string::npos;
|
||||
if (ret) { return EncodingType::Zstd; }
|
||||
if (type == EncodingType::None && case_ignore::equal(name, "zstd")) {
|
||||
type = EncodingType::Zstd;
|
||||
}
|
||||
#endif
|
||||
|
||||
return EncodingType::None;
|
||||
if (type == EncodingType::None) { return; }
|
||||
|
||||
// Higher q-value wins; for equal q, server preference breaks ties
|
||||
if (quality > best_q ||
|
||||
(quality == best_q && priority(type) < priority(best))) {
|
||||
best_q = quality;
|
||||
best = type;
|
||||
}
|
||||
});
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
inline bool nocompressor::compress(const char *data, size_t data_length,
|
||||
@@ -6734,6 +6877,21 @@ create_decompressor(const std::string &encoding) {
|
||||
return decompressor;
|
||||
}
|
||||
|
||||
// Returns the best available compressor and its Content-Encoding name.
|
||||
// Priority: Brotli > Gzip > Zstd (matches server-side preference).
|
||||
inline std::pair<std::unique_ptr<compressor>, const char *>
|
||||
create_compressor() {
|
||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||
return {detail::make_unique<brotli_compressor>(), "br"};
|
||||
#elif defined(CPPHTTPLIB_ZLIB_SUPPORT)
|
||||
return {detail::make_unique<gzip_compressor>(), "gzip"};
|
||||
#elif defined(CPPHTTPLIB_ZSTD_SUPPORT)
|
||||
return {detail::make_unique<zstd_compressor>(), "zstd"};
|
||||
#else
|
||||
return {nullptr, nullptr};
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool is_prohibited_header_name(const std::string &name) {
|
||||
using udl::operator""_t;
|
||||
|
||||
@@ -6813,6 +6971,16 @@ inline bool read_headers(Stream &strm, Headers &headers) {
|
||||
header_count++;
|
||||
}
|
||||
|
||||
// RFC 9110 Section 8.6: Reject requests with multiple Content-Length
|
||||
// headers that have different values to prevent request smuggling.
|
||||
auto cl_range = headers.equal_range("Content-Length");
|
||||
if (cl_range.first != cl_range.second) {
|
||||
const auto &first_val = cl_range.first->second;
|
||||
for (auto it = std::next(cl_range.first); it != cl_range.second; ++it) {
|
||||
if (it->second != first_val) { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7556,7 +7724,7 @@ inline bool parse_accept_header(const std::string &s,
|
||||
struct AcceptEntry {
|
||||
std::string media_type;
|
||||
double quality;
|
||||
int order; // Original order in header
|
||||
int order;
|
||||
};
|
||||
|
||||
std::vector<AcceptEntry> entries;
|
||||
@@ -7574,48 +7742,12 @@ inline bool parse_accept_header(const std::string &s,
|
||||
}
|
||||
|
||||
AcceptEntry accept_entry;
|
||||
accept_entry.quality = 1.0; // Default quality
|
||||
accept_entry.order = order++;
|
||||
|
||||
// Find q= parameter
|
||||
auto q_pos = entry.find(";q=");
|
||||
if (q_pos == std::string::npos) { q_pos = entry.find("; q="); }
|
||||
|
||||
if (q_pos != std::string::npos) {
|
||||
// Extract media type (before q parameter)
|
||||
accept_entry.media_type = trim_copy(entry.substr(0, q_pos));
|
||||
|
||||
// Extract quality value
|
||||
auto q_start = entry.find('=', q_pos) + 1;
|
||||
auto q_end = entry.find(';', q_start);
|
||||
if (q_end == std::string::npos) { q_end = entry.length(); }
|
||||
|
||||
std::string quality_str =
|
||||
trim_copy(entry.substr(q_start, q_end - q_start));
|
||||
if (quality_str.empty()) {
|
||||
has_invalid_entry = true;
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
double v = 0.0;
|
||||
auto res = detail::from_chars(
|
||||
quality_str.data(), quality_str.data() + quality_str.size(), v);
|
||||
if (res.ec == std::errc{}) {
|
||||
accept_entry.quality = v;
|
||||
} else {
|
||||
has_invalid_entry = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check if quality is in valid range [0.0, 1.0]
|
||||
if (accept_entry.quality < 0.0 || accept_entry.quality > 1.0) {
|
||||
has_invalid_entry = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// No quality parameter, use entire entry as media type
|
||||
accept_entry.media_type = entry;
|
||||
if (!parse_quality(entry.data(), entry.data() + entry.size(),
|
||||
accept_entry.media_type, accept_entry.quality)) {
|
||||
has_invalid_entry = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove additional parameters from media type
|
||||
@@ -9268,7 +9400,8 @@ inline std::string decode_path_component(const std::string &component) {
|
||||
// Unicode %uXXXX encoding
|
||||
auto val = 0;
|
||||
if (detail::from_hex_to_i(component, i + 2, 4, val)) {
|
||||
// 4 digits Unicode codes
|
||||
// 4 digits Unicode codes: val is 0x0000-0xFFFF (from 4 hex digits),
|
||||
// so to_utf8 writes at most 3 bytes. buff[4] is safe.
|
||||
char buff[4];
|
||||
size_t len = detail::to_utf8(val, buff);
|
||||
if (len > 0) { result.append(buff, len); }
|
||||
@@ -9373,6 +9506,30 @@ inline std::string decode_query_component(const std::string &component,
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string sanitize_filename(const std::string &filename) {
|
||||
// Extract basename: find the last path separator (/ or \)
|
||||
auto pos = filename.find_last_of("/\\");
|
||||
auto result =
|
||||
(pos != std::string::npos) ? filename.substr(pos + 1) : filename;
|
||||
|
||||
// Strip null bytes
|
||||
result.erase(std::remove(result.begin(), result.end(), '\0'), result.end());
|
||||
|
||||
// Trim whitespace
|
||||
{
|
||||
auto start = result.find_first_not_of(" \t");
|
||||
auto end = result.find_last_not_of(" \t");
|
||||
result = (start == std::string::npos)
|
||||
? ""
|
||||
: result.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
// Reject . and ..
|
||||
if (result == "." || result == "..") { return ""; }
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string append_query_params(const std::string &path,
|
||||
const Params ¶ms) {
|
||||
std::string path_with_query = path;
|
||||
@@ -10501,7 +10658,18 @@ inline bool Server::set_mount_point(const std::string &mount_point,
|
||||
if (stat.is_dir()) {
|
||||
std::string mnt = !mount_point.empty() ? mount_point : "/";
|
||||
if (!mnt.empty() && mnt[0] == '/') {
|
||||
base_dirs_.push_back({std::move(mnt), dir, std::move(headers)});
|
||||
std::string resolved_base;
|
||||
if (detail::canonicalize_path(dir.c_str(), resolved_base)) {
|
||||
#if defined(_WIN32)
|
||||
if (resolved_base.back() != '\\' && resolved_base.back() != '/') {
|
||||
resolved_base += '\\';
|
||||
}
|
||||
#else
|
||||
if (resolved_base.back() != '/') { resolved_base += '/'; }
|
||||
#endif
|
||||
}
|
||||
base_dirs_.push_back(
|
||||
{std::move(mnt), dir, std::move(resolved_base), std::move(headers)});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -10661,6 +10829,20 @@ inline Server &Server::set_payload_max_length(size_t length) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline Server &Server::set_websocket_ping_interval(time_t sec) {
|
||||
websocket_ping_interval_sec_ = sec;
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <class Rep, class Period>
|
||||
inline Server &Server::set_websocket_ping_interval(
|
||||
const std::chrono::duration<Rep, Period> &duration) {
|
||||
detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t /*usec*/) {
|
||||
set_websocket_ping_interval(sec);
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline bool Server::bind_to_port(const std::string &host, int port,
|
||||
int socket_flags) {
|
||||
auto ret = bind_internal(host, port, socket_flags);
|
||||
@@ -11081,6 +11263,18 @@ inline bool Server::handle_file_request(Request &req, Response &res) {
|
||||
auto path = entry.base_dir + sub_path;
|
||||
if (path.back() == '/') { path += "index.html"; }
|
||||
|
||||
// Defense-in-depth: is_valid_path blocks ".." traversal in the URL,
|
||||
// but symlinks/junctions can still escape the base directory.
|
||||
if (!entry.resolved_base_dir.empty()) {
|
||||
std::string resolved_path;
|
||||
if (detail::canonicalize_path(path.c_str(), resolved_path) &&
|
||||
!detail::is_path_within_base(resolved_path,
|
||||
entry.resolved_base_dir)) {
|
||||
res.status = StatusCode::Forbidden_403;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
detail::FileStat stat(path);
|
||||
|
||||
if (stat.is_dir()) {
|
||||
@@ -11319,6 +11513,10 @@ inline bool Server::listen_internal() {
|
||||
detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO,
|
||||
write_timeout_sec_, write_timeout_usec_);
|
||||
|
||||
if (tcp_nodelay_) {
|
||||
detail::set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1);
|
||||
}
|
||||
|
||||
if (!task_queue->enqueue(
|
||||
[this, sock]() { process_and_close_socket(sock); })) {
|
||||
output_error_log(Error::ResourceExhaustion, nullptr);
|
||||
@@ -11795,7 +11993,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
{
|
||||
// Use WebSocket-specific read timeout instead of HTTP timeout
|
||||
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0);
|
||||
ws::WebSocket ws(strm, req, true);
|
||||
ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_);
|
||||
entry.handler(req, ws);
|
||||
}
|
||||
return true;
|
||||
@@ -12039,6 +12237,13 @@ inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
|
||||
return create_and_connect_socket(socket, error);
|
||||
}
|
||||
|
||||
inline bool ClientImpl::setup_proxy_connection(
|
||||
Socket & /*socket*/,
|
||||
std::chrono::time_point<std::chrono::steady_clock> /*start_time*/,
|
||||
Response & /*res*/, bool & /*success*/, Error & /*error*/) {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void ClientImpl::shutdown_ssl(Socket & /*socket*/,
|
||||
bool /*shutdown_gracefully*/) {
|
||||
// If there are any requests in flight from threads other than us, then it's
|
||||
@@ -12160,27 +12365,14 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
// TODO: refactoring
|
||||
if (is_ssl()) {
|
||||
auto &scli = static_cast<SSLClient &>(*this);
|
||||
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||
auto success = false;
|
||||
if (!scli.connect_with_proxy(socket_, req.start_time_, res, success,
|
||||
error)) {
|
||||
if (!success) { output_error_log(error, &req); }
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||
if (!scli.initialize_ssl(socket_, error)) {
|
||||
output_error_log(error, &req);
|
||||
return false;
|
||||
}
|
||||
{
|
||||
auto success = true;
|
||||
if (!setup_proxy_connection(socket_, req.start_time_, res, success,
|
||||
error)) {
|
||||
if (!success) { output_error_log(error, &req); }
|
||||
return success;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Mark the current socket as being in use so that it cannot be closed by
|
||||
@@ -12341,17 +12533,15 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
||||
return handle;
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
if (is_ssl()) {
|
||||
auto &scli = static_cast<SSLClient &>(*this);
|
||||
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||
if (!scli.initialize_ssl(socket_, handle.error)) {
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
{
|
||||
auto success = true;
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
if (!setup_proxy_connection(socket_, start_time, *handle.response,
|
||||
success, handle.error)) {
|
||||
if (!success) { handle.response.reset(); }
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
transfer_socket_ownership_to_handle(handle);
|
||||
@@ -12630,7 +12820,7 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
||||
|
||||
if (res.get_header_value("Connection") == "close" ||
|
||||
(res.version == "HTTP/1.0" && res.reason != "Connection established")) {
|
||||
// TODO this requires a not-entirely-obvious chain of calls to be correct
|
||||
// NOTE: this requires a not-entirely-obvious chain of calls to be correct
|
||||
// for this to be safe.
|
||||
|
||||
// This is safe to call because handle_request is only called by send_
|
||||
@@ -12708,7 +12898,7 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
||||
|
||||
auto next_port = port_;
|
||||
if (!port_str.empty()) {
|
||||
next_port = std::stoi(port_str);
|
||||
if (!detail::parse_port(port_str, next_port)) { return false; }
|
||||
} else if (!next_scheme.empty()) {
|
||||
next_port = next_scheme == "https" ? 443 : 80;
|
||||
}
|
||||
@@ -12759,18 +12949,10 @@ inline bool ClientImpl::create_redirect_client(
|
||||
// Setup basic client configuration first
|
||||
setup_redirect_client(redirect_client);
|
||||
|
||||
// SSL-specific configuration for proxy environments
|
||||
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||
// Critical: Disable SSL verification for proxy environments
|
||||
redirect_client.enable_server_certificate_verification(false);
|
||||
redirect_client.enable_server_hostname_verification(false);
|
||||
} else {
|
||||
// For direct SSL connections, copy SSL verification settings
|
||||
redirect_client.enable_server_certificate_verification(
|
||||
server_certificate_verification_);
|
||||
redirect_client.enable_server_hostname_verification(
|
||||
server_hostname_verification_);
|
||||
}
|
||||
redirect_client.enable_server_certificate_verification(
|
||||
server_certificate_verification_);
|
||||
redirect_client.enable_server_hostname_verification(
|
||||
server_hostname_verification_);
|
||||
|
||||
// Transfer CA certificate to redirect client
|
||||
if (!ca_cert_pem_.empty()) {
|
||||
@@ -12877,14 +13059,9 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
|
||||
auto is_shutting_down = []() { return false; };
|
||||
|
||||
if (req.is_chunked_content_provider_) {
|
||||
// TODO: Brotli support
|
||||
std::unique_ptr<detail::compressor> compressor;
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
if (compress_) {
|
||||
compressor = detail::make_unique<detail::gzip_compressor>();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
auto compressor = compress_ ? detail::create_compressor().first
|
||||
: std::unique_ptr<detail::compressor>();
|
||||
if (!compressor) {
|
||||
compressor = detail::make_unique<detail::nocompressor>();
|
||||
}
|
||||
|
||||
@@ -13115,14 +13292,15 @@ ClientImpl::send_with_content_provider_and_receiver(
|
||||
Error &error) {
|
||||
if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
|
||||
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
if (compress_) { req.set_header("Content-Encoding", "gzip"); }
|
||||
#endif
|
||||
auto enc = compress_
|
||||
? detail::create_compressor()
|
||||
: std::pair<std::unique_ptr<detail::compressor>, const char *>(
|
||||
nullptr, nullptr);
|
||||
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
if (compress_ && !content_provider_without_length) {
|
||||
// TODO: Brotli support
|
||||
detail::gzip_compressor compressor;
|
||||
if (enc.second) { req.set_header("Content-Encoding", enc.second); }
|
||||
|
||||
if (enc.first && !content_provider_without_length) {
|
||||
auto &compressor = enc.first;
|
||||
|
||||
if (content_provider) {
|
||||
auto ok = true;
|
||||
@@ -13133,7 +13311,7 @@ ClientImpl::send_with_content_provider_and_receiver(
|
||||
if (ok) {
|
||||
auto last = offset + data_len == content_length;
|
||||
|
||||
auto ret = compressor.compress(
|
||||
auto ret = compressor->compress(
|
||||
data, data_len, last,
|
||||
[&](const char *compressed_data, size_t compressed_data_len) {
|
||||
req.body.append(compressed_data, compressed_data_len);
|
||||
@@ -13157,19 +13335,17 @@ ClientImpl::send_with_content_provider_and_receiver(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!compressor.compress(body, content_length, true,
|
||||
[&](const char *data, size_t data_len) {
|
||||
req.body.append(data, data_len);
|
||||
return true;
|
||||
})) {
|
||||
if (!compressor->compress(body, content_length, true,
|
||||
[&](const char *data, size_t data_len) {
|
||||
req.body.append(data, data_len);
|
||||
return true;
|
||||
})) {
|
||||
error = Error::Compression;
|
||||
output_error_log(error, &req);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
} else {
|
||||
if (content_provider) {
|
||||
req.content_length_ = content_length;
|
||||
req.content_provider_ = std::move(content_provider);
|
||||
@@ -14487,7 +14663,8 @@ inline Client::Client(const std::string &scheme_host_port,
|
||||
if (host.empty()) { host = m[3].str(); }
|
||||
|
||||
auto port_str = m[4].str();
|
||||
auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);
|
||||
auto port = is_ssl ? 443 : 80;
|
||||
if (!port_str.empty() && !detail::parse_port(port_str, port)) { return; }
|
||||
|
||||
if (is_ssl) {
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
@@ -15335,6 +15512,24 @@ inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) {
|
||||
return ClientImpl::create_and_connect_socket(socket, error);
|
||||
}
|
||||
|
||||
inline bool SSLClient::setup_proxy_connection(
|
||||
Socket &socket,
|
||||
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
||||
Response &res, bool &success, Error &error) {
|
||||
if (proxy_host_.empty() || proxy_port_ == -1) { return true; }
|
||||
|
||||
if (!connect_with_proxy(socket, start_time, res, success, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!initialize_ssl(socket, error)) {
|
||||
success = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Assumes that socket_mutex_ is locked and that there are no requests in
|
||||
// flight
|
||||
inline bool SSLClient::connect_with_proxy(
|
||||
@@ -19851,11 +20046,11 @@ inline WebSocket::~WebSocket() {
|
||||
}
|
||||
|
||||
inline void WebSocket::start_heartbeat() {
|
||||
if (ping_interval_sec_ == 0) { return; }
|
||||
ping_thread_ = std::thread([this]() {
|
||||
std::unique_lock<std::mutex> lock(ping_mutex_);
|
||||
while (!closed_) {
|
||||
ping_cv_.wait_for(lock, std::chrono::seconds(
|
||||
CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND));
|
||||
ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_));
|
||||
if (closed_) { break; }
|
||||
lock.unlock();
|
||||
if (!send_frame(Opcode::Ping, nullptr, 0)) {
|
||||
@@ -19900,7 +20095,8 @@ inline WebSocketClient::WebSocketClient(
|
||||
if (host_.empty()) { host_ = m[3].str(); }
|
||||
|
||||
auto port_str = m[4].str();
|
||||
port_ = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);
|
||||
port_ = is_ssl ? 443 : 80;
|
||||
if (!port_str.empty() && !detail::parse_port(port_str, port_)) { return; }
|
||||
|
||||
path_ = m[5].str();
|
||||
|
||||
@@ -19992,7 +20188,8 @@ inline bool WebSocketClient::connect() {
|
||||
Request req;
|
||||
req.method = "GET";
|
||||
req.path = path_;
|
||||
ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false));
|
||||
ws_ = std::unique_ptr<WebSocket>(
|
||||
new WebSocket(std::move(strm), req, false, websocket_ping_interval_sec_));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -20032,6 +20229,10 @@ inline void WebSocketClient::set_write_timeout(time_t sec, time_t usec) {
|
||||
write_timeout_usec_ = usec;
|
||||
}
|
||||
|
||||
inline void WebSocketClient::set_websocket_ping_interval(time_t sec) {
|
||||
websocket_ping_interval_sec_ = sec;
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
|
||||
inline void WebSocketClient::set_ca_cert_path(const std::string &path) {
|
||||
|
||||
@@ -36,6 +36,7 @@ others:
|
||||
@(cd test && make fuzz_test)
|
||||
@(cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat)
|
||||
@(cd test && make test_thread_pool && ./test_thread_pool)
|
||||
@(cd test && make test_benchmark && ./test_benchmark)
|
||||
|
||||
build:
|
||||
@(cd test && make test_split)
|
||||
|
||||
+11
-1
@@ -219,6 +219,16 @@ style_check: $(STYLE_CHECK_FILES)
|
||||
echo "All files are properly formatted."; \
|
||||
fi
|
||||
|
||||
BENCHMARK_LIBS = -lpthread
|
||||
ifneq ($(OS), Windows_NT)
|
||||
ifeq ($(shell uname -s), Darwin)
|
||||
BENCHMARK_LIBS += -framework CoreFoundation -framework CFNetwork
|
||||
endif
|
||||
endif
|
||||
|
||||
test_benchmark : test_benchmark.cc ../httplib.h Makefile
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_benchmark.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(BENCHMARK_LIBS)
|
||||
|
||||
test_websocket_heartbeat : test_websocket_heartbeat.cc ../httplib.h Makefile
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_websocket_heartbeat.cc $(TEST_ARGS)
|
||||
@file $@
|
||||
@@ -254,5 +264,5 @@ cert.pem:
|
||||
./gen-certs.sh
|
||||
|
||||
clean:
|
||||
rm -rf test test_split test_mbedtls test_split_mbedtls test_wolfssl test_split_wolfssl test_no_tls, test_split_no_tls test_proxy test_proxy_mbedtls test_proxy_wolfssl server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log cpp-httplib
|
||||
rm -rf test test_split test_mbedtls test_split_mbedtls test_wolfssl test_split_wolfssl test_no_tls, test_split_no_tls test_proxy test_proxy_mbedtls test_proxy_wolfssl test_benchmark server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log cpp-httplib
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ acl localnet src fc00::/7 # RFC 4193 local private network range
|
||||
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||
|
||||
acl SSL_ports port 443
|
||||
acl SSL_ports port 1025-65535
|
||||
acl Safe_ports port 80 # http
|
||||
acl Safe_ports port 21 # ftp
|
||||
acl Safe_ports port 443 # https
|
||||
|
||||
@@ -15,6 +15,7 @@ acl localnet src fc00::/7 # RFC 4193 local private network range
|
||||
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||
|
||||
acl SSL_ports port 443
|
||||
acl SSL_ports port 1025-65535
|
||||
acl Safe_ports port 80 # http
|
||||
acl Safe_ports port 21 # ftp
|
||||
acl Safe_ports port 443 # https
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
squid_basic:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
squid_digest:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
+317
-189
@@ -102,71 +102,6 @@ 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) {
|
||||
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()); }
|
||||
@@ -413,6 +348,38 @@ TEST(DecodePathTest, PercentCharacterNUL) {
|
||||
EXPECT_EQ(decode_path_component("x%00x"), expected);
|
||||
}
|
||||
|
||||
TEST(DecodePathTest, UnicodeEncoding) {
|
||||
// %u0041 = 'A' (1-byte UTF-8)
|
||||
EXPECT_EQ("A", decode_path_component("%u0041"));
|
||||
// %u00E9 = 'é' (2-byte UTF-8)
|
||||
EXPECT_EQ(U8("é"), decode_path_component("%u00E9"));
|
||||
// %u3042 = 'あ' (3-byte UTF-8)
|
||||
EXPECT_EQ(U8("あ"), decode_path_component("%u3042"));
|
||||
// %uFFFF = max 4-digit hex (3-byte UTF-8, must not overflow buff[4])
|
||||
EXPECT_FALSE(decode_path_component("%uFFFF").empty());
|
||||
// %uD800 = surrogate (invalid, silently dropped)
|
||||
EXPECT_EQ("", decode_path_component("%uD800"));
|
||||
}
|
||||
|
||||
TEST(SanitizeFilenameTest, VariousPatterns) {
|
||||
// Path traversal
|
||||
EXPECT_EQ("passwd", httplib::sanitize_filename("../../../etc/passwd"));
|
||||
EXPECT_EQ("passwd", httplib::sanitize_filename("..\\..\\etc\\passwd"));
|
||||
EXPECT_EQ("file.txt", httplib::sanitize_filename("path/to\\..\\file.txt"));
|
||||
// Normal and edge cases
|
||||
EXPECT_EQ("photo.jpg", httplib::sanitize_filename("photo.jpg"));
|
||||
EXPECT_EQ("filename.txt",
|
||||
httplib::sanitize_filename("/path/to/filename.txt"));
|
||||
EXPECT_EQ(".gitignore", httplib::sanitize_filename(".gitignore"));
|
||||
EXPECT_EQ("", httplib::sanitize_filename(".."));
|
||||
EXPECT_EQ("", httplib::sanitize_filename(""));
|
||||
// Null bytes stripped
|
||||
EXPECT_EQ("safe.txt",
|
||||
httplib::sanitize_filename(std::string("safe\0.txt", 9)));
|
||||
// Whitespace-only rejected
|
||||
EXPECT_EQ("", httplib::sanitize_filename(" "));
|
||||
}
|
||||
|
||||
TEST(EncodeQueryParamTest, ParseUnescapedChararactersTest) {
|
||||
string unescapedCharacters = "-_.!~*'()";
|
||||
|
||||
@@ -1235,6 +1202,89 @@ TEST(ParseAcceptEncoding3, AcceptEncoding) {
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(ParseAcceptEncoding4, AcceptEncodingQZero) {
|
||||
// All supported encodings rejected with q=0 should return None
|
||||
Request req;
|
||||
req.set_header("Accept-Encoding", "gzip;q=0, br;q=0, zstd;q=0, deflate");
|
||||
|
||||
Response res;
|
||||
res.set_header("Content-Type", "text/plain");
|
||||
|
||||
auto ret = detail::encoding_type(req, res);
|
||||
|
||||
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||
}
|
||||
|
||||
TEST(ParseAcceptEncoding5, AcceptEncodingQZeroVariants) {
|
||||
// q=0.0, q=0.00, q=0.000 should also be treated as rejected
|
||||
Request req;
|
||||
req.set_header("Accept-Encoding", "gzip;q=0.000, br;q=0.0, zstd;q=0.00");
|
||||
|
||||
Response res;
|
||||
res.set_header("Content-Type", "text/plain");
|
||||
|
||||
auto ret = detail::encoding_type(req, res);
|
||||
|
||||
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||
}
|
||||
|
||||
TEST(ParseAcceptEncoding6, AcceptEncodingXGzipQZero) {
|
||||
// x-gzip;q=0 should not cause "gzip" to be incorrectly detected
|
||||
Request req;
|
||||
req.set_header("Accept-Encoding", "x-gzip;q=0");
|
||||
|
||||
Response res;
|
||||
res.set_header("Content-Type", "text/plain");
|
||||
|
||||
auto ret = detail::encoding_type(req, res);
|
||||
|
||||
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||
}
|
||||
|
||||
TEST(ParseAcceptEncoding7, AcceptEncodingCaseInsensitive) {
|
||||
// RFC 7231: Accept-Encoding values are case-insensitive
|
||||
Request req;
|
||||
req.set_header("Accept-Encoding", "GZIP, BR, ZSTD");
|
||||
|
||||
Response res;
|
||||
res.set_header("Content-Type", "text/plain");
|
||||
|
||||
auto ret = detail::encoding_type(req, res);
|
||||
|
||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||
EXPECT_TRUE(ret == detail::EncodingType::Brotli);
|
||||
#elif CPPHTTPLIB_ZLIB_SUPPORT
|
||||
EXPECT_TRUE(ret == detail::EncodingType::Gzip);
|
||||
#elif CPPHTTPLIB_ZSTD_SUPPORT
|
||||
EXPECT_TRUE(ret == detail::EncodingType::Zstd);
|
||||
#else
|
||||
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(ParseAcceptEncoding8, AcceptEncodingQValuePriority) {
|
||||
// q value should determine priority, not hardcoded order
|
||||
Request req;
|
||||
req.set_header("Accept-Encoding", "br;q=0.5, gzip;q=1.0, zstd;q=0.8");
|
||||
|
||||
Response res;
|
||||
res.set_header("Content-Type", "text/plain");
|
||||
|
||||
auto ret = detail::encoding_type(req, res);
|
||||
|
||||
// gzip has highest q=1.0, so it should be selected even though
|
||||
// br and zstd are also supported
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
EXPECT_TRUE(ret == detail::EncodingType::Gzip);
|
||||
#elif CPPHTTPLIB_ZSTD_SUPPORT
|
||||
EXPECT_TRUE(ret == detail::EncodingType::Zstd);
|
||||
#elif CPPHTTPLIB_BROTLI_SUPPORT
|
||||
EXPECT_TRUE(ret == detail::EncodingType::Brotli);
|
||||
#else
|
||||
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(BufferStreamTest, read) {
|
||||
detail::BufferStream strm1;
|
||||
Stream &strm = strm1;
|
||||
@@ -2330,6 +2380,31 @@ TEST(RedirectToDifferentPort, Redirect) {
|
||||
EXPECT_EQ("Hello World!", res->body);
|
||||
}
|
||||
|
||||
TEST(RedirectToDifferentPort, OverflowPortNumber) {
|
||||
Server svr;
|
||||
svr.Get("/redir", [&](const Request & /*req*/, Response &res) {
|
||||
// Port number that overflows int — should not crash
|
||||
res.set_redirect("http://localhost:99999999999999999999/target");
|
||||
});
|
||||
|
||||
auto port = svr.bind_to_any_port(HOST);
|
||||
auto thread = std::thread([&]() { svr.listen_after_bind(); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli(HOST, port);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/redir");
|
||||
// Should fail gracefully, not crash (no valid response due to bad port)
|
||||
EXPECT_FALSE(res);
|
||||
}
|
||||
|
||||
TEST(RedirectFromPageWithContent, Redirect) {
|
||||
Server svr;
|
||||
|
||||
@@ -2512,7 +2587,7 @@ TEST(PathUrlEncodeTest, IncludePercentEncodingLF) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BindServerTest, DISABLED_BindDualStack) {
|
||||
TEST(BindServerTest, BindDualStack) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/1", [&](const Request & /*req*/, Response &res) {
|
||||
@@ -5605,7 +5680,13 @@ TEST_F(ServerTest, PutLargeFileWithGzip2) {
|
||||
// depending on the zlib library.
|
||||
EXPECT_LT(res.get_request_header_value_u64("Content-Length"),
|
||||
static_cast<uint64_t>(10 * 1024 * 1024));
|
||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||
EXPECT_EQ("br", res.get_request_header_value("Content-Encoding"));
|
||||
#elif defined(CPPHTTPLIB_ZLIB_SUPPORT)
|
||||
EXPECT_EQ("gzip", res.get_request_header_value("Content-Encoding"));
|
||||
#elif defined(CPPHTTPLIB_ZSTD_SUPPORT)
|
||||
EXPECT_EQ("zstd", res.get_request_header_value("Content-Encoding"));
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, PutContentWithDeflate) {
|
||||
@@ -5746,53 +5827,6 @@ TEST(GzipDecompressor, DeflateDecompressionTrailingBytes) {
|
||||
ASSERT_EQ(original_text, decompressed_data);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
TEST(GzipDecompressor, LargeRandomData) {
|
||||
|
||||
// prepare large random data that is difficult to be compressed and is
|
||||
// expected to have large size even when compressed
|
||||
std::random_device seed_gen;
|
||||
std::mt19937 random(seed_gen());
|
||||
constexpr auto large_size_byte = 4294967296UL; // 4GiB
|
||||
constexpr auto data_size = large_size_byte + 134217728UL; // + 128MiB
|
||||
std::vector<std::uint32_t> data(data_size / sizeof(std::uint32_t));
|
||||
std::generate(data.begin(), data.end(), [&]() { return random(); });
|
||||
|
||||
// compress data over 4GiB
|
||||
std::string compressed_data;
|
||||
compressed_data.reserve(large_size_byte + 536870912UL); // + 512MiB reserved
|
||||
httplib::detail::gzip_compressor compressor;
|
||||
auto result = compressor.compress(reinterpret_cast<const char *>(data.data()),
|
||||
data.size() * sizeof(std::uint32_t), true,
|
||||
[&](const char *data, size_t size) {
|
||||
compressed_data.insert(
|
||||
compressed_data.size(), data, size);
|
||||
return true;
|
||||
});
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// FIXME: compressed data size is expected to be greater than 4GiB,
|
||||
// but there is no guarantee
|
||||
// ASSERT_TRUE(compressed_data.size() >= large_size_byte);
|
||||
|
||||
// decompress data over 4GiB
|
||||
std::string decompressed_data;
|
||||
decompressed_data.reserve(data_size);
|
||||
httplib::detail::gzip_decompressor decompressor;
|
||||
result = decompressor.decompress(
|
||||
compressed_data.data(), compressed_data.size(),
|
||||
[&](const char *data, size_t size) {
|
||||
decompressed_data.insert(decompressed_data.size(), data, size);
|
||||
return true;
|
||||
});
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// compare
|
||||
ASSERT_EQ(data_size, decompressed_data.size());
|
||||
ASSERT_TRUE(std::memcmp(data.data(), decompressed_data.data(), data_size) ==
|
||||
0);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||
@@ -5815,6 +5849,47 @@ TEST_F(ServerTest, GetStreamedChunkedWithBrotli2) {
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ(std::string("123456789"), res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, PutWithContentProviderWithBrotli) {
|
||||
cli_.set_compress(true);
|
||||
auto res = cli_.Put(
|
||||
"/put", 3,
|
||||
[](size_t /*offset*/, size_t /*length*/, DataSink &sink) {
|
||||
sink.os << "PUT";
|
||||
return true;
|
||||
},
|
||||
"text/plain");
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ("PUT", res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, PutWithContentProviderWithoutLengthWithBrotli) {
|
||||
cli_.set_compress(true);
|
||||
auto res = cli_.Put(
|
||||
"/put",
|
||||
[](size_t /*offset*/, DataSink &sink) {
|
||||
sink.os << "PUT";
|
||||
sink.done();
|
||||
return true;
|
||||
},
|
||||
"text/plain");
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ("PUT", res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, PutLargeFileWithBrotli) {
|
||||
cli_.set_compress(true);
|
||||
auto res = cli_.Put("/put-large", LARGE_DATA, "text/plain");
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ(LARGE_DATA, res->body);
|
||||
EXPECT_EQ("br", res.get_request_header_value("Content-Encoding"));
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_F(ServerTest, Patch) {
|
||||
@@ -9441,6 +9516,18 @@ TEST(HostAndPortPropertiesTest, NoSSLWithSimpleAPI) {
|
||||
ASSERT_EQ(1234, cli.port());
|
||||
}
|
||||
|
||||
TEST(HostAndPortPropertiesTest, OverflowPortNumber) {
|
||||
// Port number that overflows int — should not crash, client becomes invalid
|
||||
httplib::Client cli("http://www.google.com:99999999999999999999");
|
||||
ASSERT_FALSE(cli.is_valid());
|
||||
}
|
||||
|
||||
TEST(HostAndPortPropertiesTest, PortOutOfRange) {
|
||||
// Port 99999 exceeds valid range (1-65535) — should not crash
|
||||
httplib::Client cli("http://www.google.com:99999");
|
||||
ASSERT_FALSE(cli.is_valid());
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(HostAndPortPropertiesTest, SSL) {
|
||||
httplib::SSLClient cli("www.google.com");
|
||||
@@ -10713,38 +10800,6 @@ TEST(ClientImplMethods, GetSocketTest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled due to out-of-memory problem on GitHub Actions
|
||||
#ifdef _WIN64
|
||||
TEST(ServerLargeContentTest, DISABLED_SendLargeContent) {
|
||||
// allocate content size larger than 2GB in memory
|
||||
const size_t content_size = 2LL * 1024LL * 1024LL * 1024LL + 1LL;
|
||||
char *content = (char *)malloc(content_size);
|
||||
ASSERT_TRUE(content);
|
||||
|
||||
Server svr;
|
||||
svr.Get("/foo",
|
||||
[=](const httplib::Request & /*req*/, httplib::Response &res) {
|
||||
res.set_content(content, content_size, "application/octet-stream");
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&svr]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
if (content) free(content);
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli(HOST, PORT);
|
||||
auto res = cli.Get("/foo");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ(content_size, res->body.length());
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(YahooRedirectTest2, SimpleInterface_Online) {
|
||||
Client cli("http://yahoo.com");
|
||||
@@ -13051,6 +13106,66 @@ TEST(ClientInThreadTest, Issue2068) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RequestSmugglingTest, DuplicateContentLengthDifferentValues) {
|
||||
auto handled = false;
|
||||
|
||||
Server svr;
|
||||
svr.Post("/test", [&](const Request &, Response &) { handled = true; });
|
||||
|
||||
thread t = thread([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
ASSERT_FALSE(handled);
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
// Two Content-Length headers with different values — must be rejected
|
||||
auto req = "POST /test HTTP/1.1\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"Content-Length: 10\r\n"
|
||||
"\r\n"
|
||||
"hello";
|
||||
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
ASSERT_EQ("HTTP/1.1 400 Bad Request",
|
||||
response.substr(0, response.find("\r\n")));
|
||||
}
|
||||
|
||||
TEST(RequestSmugglingTest, DuplicateContentLengthSameValues) {
|
||||
auto handled = false;
|
||||
|
||||
Server svr;
|
||||
svr.Post("/test", [&](const Request &, Response &res) {
|
||||
handled = true;
|
||||
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());
|
||||
ASSERT_TRUE(handled);
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
// Two Content-Length headers with same value — should be accepted (RFC 9110)
|
||||
auto req = "POST /test HTTP/1.1\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"\r\n"
|
||||
"hello";
|
||||
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
ASSERT_EQ("HTTP/1.1 200 OK", response.substr(0, response.find("\r\n")));
|
||||
}
|
||||
|
||||
TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) {
|
||||
Server svr;
|
||||
|
||||
@@ -13788,7 +13903,7 @@ TEST(OpenStreamMalformedContentLength, OutOfRange) {
|
||||
#endif
|
||||
|
||||
auto server_thread = serve_single_response(
|
||||
PORT + 2, "HTTP/1.1 200 OK\r\n"
|
||||
PORT + 4, "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/plain\r\n"
|
||||
"Content-Length: 99999999999999999999999999\r\n"
|
||||
"Connection: close\r\n"
|
||||
@@ -13801,7 +13916,7 @@ TEST(OpenStreamMalformedContentLength, OutOfRange) {
|
||||
// crash the process. After the fix, strtoull silently clamps to
|
||||
// ULLONG_MAX so the stream opens without crashing. The important thing
|
||||
// is that the process does NOT terminate.
|
||||
Client cli("127.0.0.1", PORT + 2);
|
||||
Client cli("127.0.0.1", PORT + 4);
|
||||
auto handle = cli.open_stream("GET", "/");
|
||||
EXPECT_TRUE(handle.is_valid());
|
||||
|
||||
@@ -16036,48 +16151,6 @@ TEST(SSLClientServerTest, FilePathConstructorSetsClientCAList) {
|
||||
EXPECT_GT(sk_X509_NAME_num(ca_list), 0);
|
||||
}
|
||||
|
||||
// Disabled due to the out-of-memory problem on GitHub Actions Workflows
|
||||
TEST(SSLClientServerTest, DISABLED_LargeDataTransfer) {
|
||||
|
||||
// prepare large data
|
||||
std::random_device seed_gen;
|
||||
std::mt19937 random(seed_gen());
|
||||
constexpr auto large_size_byte = 2147483648UL + 1048576UL; // 2GiB + 1MiB
|
||||
std::vector<std::uint32_t> binary(large_size_byte / sizeof(std::uint32_t));
|
||||
std::generate(binary.begin(), binary.end(), [&random]() { return random(); });
|
||||
|
||||
// server
|
||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
|
||||
svr.Post("/binary", [&](const Request &req, Response &res) {
|
||||
EXPECT_EQ(large_size_byte, req.body.size());
|
||||
EXPECT_EQ(0, std::memcmp(binary.data(), req.body.data(), large_size_byte));
|
||||
res.set_content(req.body, "application/octet-stream");
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
// client POST
|
||||
SSLClient cli("localhost", PORT);
|
||||
cli.enable_server_certificate_verification(false);
|
||||
cli.set_read_timeout(std::chrono::seconds(100));
|
||||
cli.set_write_timeout(std::chrono::seconds(100));
|
||||
auto res = cli.Post("/binary", reinterpret_cast<char *>(binary.data()),
|
||||
large_size_byte, "application/octet-stream");
|
||||
|
||||
// compare
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ(large_size_byte, res->body.size());
|
||||
EXPECT_EQ(0, std::memcmp(binary.data(), res->body.data(), large_size_byte));
|
||||
}
|
||||
#endif
|
||||
|
||||
// ============================================================================
|
||||
@@ -16425,6 +16498,14 @@ TEST(WebSocketTest, InvalidURL) {
|
||||
// Missing host
|
||||
ws::WebSocketClient ws4("ws://:8080/path");
|
||||
EXPECT_FALSE(ws4.is_valid());
|
||||
|
||||
// Port number overflow — should not crash
|
||||
ws::WebSocketClient ws5("ws://localhost:99999999999999999999/path");
|
||||
EXPECT_FALSE(ws5.is_valid());
|
||||
|
||||
// Port out of range
|
||||
ws::WebSocketClient ws6("ws://localhost:99999/path");
|
||||
EXPECT_FALSE(ws6.is_valid());
|
||||
}
|
||||
|
||||
TEST(WebSocketTest, UnsupportedScheme) {
|
||||
@@ -16939,3 +17020,50 @@ TEST_F(WebSocketSSLIntegrationTest, TextEcho) {
|
||||
client.close();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(_WIN32)
|
||||
TEST(SymlinkTest, SymlinkEscapeFromBaseDirectory) {
|
||||
auto secret_dir = std::string("./symlink_test_secret");
|
||||
auto served_dir = std::string("./symlink_test_served");
|
||||
auto secret_file = secret_dir + "/secret.txt";
|
||||
auto symlink_path = served_dir + "/escape";
|
||||
|
||||
// Setup: create directories and files
|
||||
mkdir(secret_dir.c_str(), 0755);
|
||||
mkdir(served_dir.c_str(), 0755);
|
||||
|
||||
{
|
||||
std::ofstream ofs(secret_file);
|
||||
ofs << "SECRET_DATA";
|
||||
}
|
||||
|
||||
// Create symlink using absolute path so it resolves correctly
|
||||
char abs_secret[PATH_MAX];
|
||||
ASSERT_NE(nullptr, realpath(secret_dir.c_str(), abs_secret));
|
||||
ASSERT_EQ(0, symlink(abs_secret, symlink_path.c_str()));
|
||||
|
||||
auto se = detail::scope_exit([&] {
|
||||
unlink(symlink_path.c_str());
|
||||
unlink(secret_file.c_str());
|
||||
rmdir(served_dir.c_str());
|
||||
rmdir(secret_dir.c_str());
|
||||
});
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/", served_dir);
|
||||
|
||||
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
|
||||
auto se2 = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
});
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", PORT);
|
||||
|
||||
// Symlink pointing outside base dir should be blocked
|
||||
auto res = cli.Get("/escape/secret.txt");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::Forbidden_403, res->status);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include <httplib.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace httplib;
|
||||
|
||||
static const int PORT = 11134;
|
||||
|
||||
static void performance_test(const char *host) {
|
||||
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"); }
|
||||
@@ -109,6 +109,55 @@ TEST(RedirectTest, YouTubeSSLDigest) {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(RedirectTest, TLSVerificationOnProxyRedirect) {
|
||||
// Untrusted HTTPS server with self-signed cert
|
||||
SSLServer untrusted_svr("cert.pem", "key.pem");
|
||||
untrusted_svr.Get("/", [](const Request &, Response &res) {
|
||||
res.set_content("MITM'd", "text/plain");
|
||||
});
|
||||
|
||||
auto untrusted_port = untrusted_svr.bind_to_any_port("0.0.0.0");
|
||||
auto t1 = thread([&]() { untrusted_svr.listen_after_bind(); });
|
||||
auto se1 = detail::scope_exit([&] {
|
||||
untrusted_svr.stop();
|
||||
t1.join();
|
||||
});
|
||||
|
||||
// HTTP server that redirects to the untrusted HTTPS server
|
||||
// Use host.docker.internal so the proxy container can reach the host
|
||||
Server redirect_svr;
|
||||
redirect_svr.Get("/", [&](const Request &, Response &res) {
|
||||
res.set_redirect(
|
||||
"https://host.docker.internal:" + to_string(untrusted_port) + "/");
|
||||
});
|
||||
|
||||
auto redirect_port = redirect_svr.bind_to_any_port("0.0.0.0");
|
||||
auto t2 = thread([&]() { redirect_svr.listen_after_bind(); });
|
||||
auto se2 = detail::scope_exit([&] {
|
||||
redirect_svr.stop();
|
||||
t2.join();
|
||||
});
|
||||
|
||||
// Wait until servers are up
|
||||
untrusted_svr.wait_until_ready();
|
||||
redirect_svr.wait_until_ready();
|
||||
|
||||
// Client with proxy + follow_location, verification ON (default)
|
||||
Client cli("host.docker.internal", redirect_port);
|
||||
cli.set_proxy("localhost", 3128);
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/");
|
||||
|
||||
// Self-signed cert must be rejected
|
||||
ASSERT_TRUE(res == nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
||||
cli.set_proxy("localhost", 3128);
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
@@ -295,3 +344,25 @@ TEST(KeepAliveTest, SSLWithDigest) {
|
||||
KeepAliveTest(cli, false);
|
||||
}
|
||||
#endif
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(ProxyTest, SSLOpenStream) {
|
||||
SSLClient cli("nghttp2.org");
|
||||
cli.set_proxy("localhost", 3128);
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
|
||||
auto handle = cli.open_stream("GET", "/httpbin/get");
|
||||
ASSERT_TRUE(handle.response);
|
||||
EXPECT_EQ(StatusCode::OK_200, handle.response->status);
|
||||
|
||||
std::string body;
|
||||
char buf[8192];
|
||||
ssize_t n;
|
||||
while ((n = handle.read(buf, sizeof(buf))) > 0) {
|
||||
body.append(buf, static_cast<size_t>(n));
|
||||
}
|
||||
EXPECT_FALSE(body.empty());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -57,6 +57,85 @@ TEST_F(WebSocketHeartbeatTest, IdleConnectionStaysAlive) {
|
||||
client.close();
|
||||
}
|
||||
|
||||
// Verify that set_websocket_ping_interval overrides the compile-time default
|
||||
TEST_F(WebSocketHeartbeatTest, RuntimePingIntervalOverride) {
|
||||
// The server is already using the compile-time default (1s).
|
||||
// Create a client with a custom runtime interval.
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
client.set_websocket_ping_interval(2);
|
||||
ASSERT_TRUE(client.connect());
|
||||
|
||||
// Sleep longer than read timeout (3s). Client heartbeat at 2s keeps alive.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(5));
|
||||
|
||||
ASSERT_TRUE(client.is_open());
|
||||
ASSERT_TRUE(client.send("runtime interval"));
|
||||
std::string msg;
|
||||
ASSERT_TRUE(client.read(msg));
|
||||
EXPECT_EQ("runtime interval", msg);
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
// Verify that ping_interval=0 disables heartbeat without breaking basic I/O.
|
||||
TEST_F(WebSocketHeartbeatTest, ZeroDisablesHeartbeat) {
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
client.set_websocket_ping_interval(0);
|
||||
ASSERT_TRUE(client.connect());
|
||||
|
||||
// Basic send/receive still works with heartbeat disabled
|
||||
ASSERT_TRUE(client.send("no client ping"));
|
||||
std::string msg;
|
||||
ASSERT_TRUE(client.read(msg));
|
||||
EXPECT_EQ("no client ping", msg);
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
// Verify that Server::set_websocket_ping_interval works at runtime
|
||||
class WebSocketServerPingIntervalTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
svr_.set_websocket_ping_interval(2);
|
||||
svr_.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
|
||||
std::string msg;
|
||||
while (ws.read(msg)) {
|
||||
ws.send(msg);
|
||||
}
|
||||
});
|
||||
|
||||
port_ = svr_.bind_to_any_port("localhost");
|
||||
thread_ = std::thread([this]() { svr_.listen_after_bind(); });
|
||||
svr_.wait_until_ready();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
svr_.stop();
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
Server svr_;
|
||||
int port_;
|
||||
std::thread thread_;
|
||||
};
|
||||
|
||||
TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
ASSERT_TRUE(client.connect());
|
||||
|
||||
// Server ping interval is 2s; client uses compile-time default (1s).
|
||||
// Both keep the connection alive.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(5));
|
||||
|
||||
ASSERT_TRUE(client.is_open());
|
||||
ASSERT_TRUE(client.send("server interval"));
|
||||
std::string msg;
|
||||
ASSERT_TRUE(client.read(msg));
|
||||
EXPECT_EQ("server interval", msg);
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
// Verify that multiple heartbeat cycles work
|
||||
TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
|
||||
Reference in New Issue
Block a user