Compare commits

...

30 Commits

Author SHA1 Message Date
yhirose 6f2717e623 Release v0.38.0 2026-03-14 23:17:13 -04:00
yhirose 257b266190 Add runtime configuration for WebSocket ping interval and related tests 2026-03-14 22:44:17 -04:00
yhirose ba0d0b82db Add benchmark tests and related configurations for performance evaluation 2026-03-14 22:16:53 -04:00
yhirose 5ecba74a99 Remove large data tests for GzipDecompressor and SSLClientServerTest due to memory issues 2026-03-14 21:50:55 -04:00
yhirose ec1ffbc27d Add Brotli compression support and corresponding tests 2026-03-14 21:42:48 -04:00
yhirose 4978f26f86 Fix port number in OpenStreamMalformedContentLength test to avoid conflicts 2026-03-14 20:59:15 -04:00
yhirose bb7c7ab075 Add quality parameter parsing for Accept-Encoding header and enhance encoding type selection logic 2026-03-14 20:51:25 -04:00
yhirose 1c3d35f83c Update comment to clarify requirements for safe handling in ClientImpl::handle_request 2026-03-14 19:27:51 -04:00
yhirose b1bb2b7ecc Implement setup_proxy_connection method for SSLClient and refactor proxy handling in open_stream 2026-03-14 19:26:31 -04:00
yhirose f6ed5fc60f Add SSL support for proxy connections in open_stream and corresponding test 2026-03-14 18:38:34 -04:00
yhirose 69d468f4d9 Enable BindDualStack test and remove disabled large content test due to memory issues 2026-03-14 18:22:27 -04:00
yhirose 2e61fd3e6e Update documentation to clarify progress callback usage and user data handling in examples 2026-03-13 22:54:29 -04:00
yhirose 3ad4a4243a Update .gitignore to include AGENTS.md and related documentation files 2026-03-13 22:31:44 -04:00
yhirose 511e3ef9e5 Update README.md to enhance TLS backend documentation and clarify platform-specific certificate handling 2026-03-13 22:30:05 -04:00
yhirose f787f31b87 Implement symlink protection in static file server and add corresponding tests 2026-03-13 16:22:16 -04:00
yhirose 43a54a3e3d Add tests for Unicode path component decoding in decode_path_component function 2026-03-13 00:32:41 -04:00
yhirose 83e98a28dd Add filename sanitization function and tests to prevent path traversal vulnerabilities 2026-03-13 00:29:13 -04:00
yhirose 4d7c9a788d Release v0.37.2 2026-03-13 00:11:00 -04:00
yhirose 1cd0347ace Refactor parse_port function to accept char pointer and length, improving flexibility and validation 2026-03-13 00:05:13 -04:00
yhirose b3a8af80b9 Add port validation and corresponding tests to prevent overflow and out-of-range values 2026-03-13 00:05:13 -04:00
yhirose 1e97c28e36 Implement request smuggling protection for duplicate Content-Length headers and add corresponding tests 2026-03-13 00:05:13 -04:00
yhirose d279eff4db Fix the proxy test error 2026-03-12 23:15:10 -04:00
yhirose 188035fb6d Add a test for the previous change 2026-03-12 22:57:11 -04:00
yhirose 125272f34b Fix TLS cert verification bypass on proxy redirect introduced in #2165 (#2396) 2026-03-12 21:54:51 -04:00
yhirose 9ced2f614d Fix #2395 2026-03-12 19:18:09 -04:00
yhirose 68fa9bce0f Release v0.37.1 2026-03-09 20:51:37 -04:00
yhirose 1f34c541b0 Add more books 2026-03-09 19:30:18 -04:00
yhirose e41ec36274 Fix handling of malformed Content-Length in open_stream and add tests 2026-03-08 22:30:20 -04:00
yhirose 7489fd3a8b Remove 32-bit limitation (#2388)
* Remove 32-bit limitation

* Fix build problems

* Add 32-bit disclaimer and fix MSVC x86 warnings

- Move 32-bit warning to top of README with strong disclaimer
- Add static_cast<size_t> to fix truncation warnings on 32-bit MSVC

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:07:21 -05:00
yhirose ab3098f18b Update README 2026-03-06 18:50:23 -05:00
31 changed files with 1457 additions and 421 deletions
+36
View File
@@ -0,0 +1,36 @@
name: 32-bit Build Test
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
test-win32:
name: Windows 32-bit (MSVC x86)
runs-on: windows-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Build (Win32)
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x86
cl /std:c++14 /EHsc /W4 /WX /c /Fo:NUL test\test_32bit_build.cpp
test-arm32:
name: ARM 32-bit (cross-compile)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Install cross compiler
run: sudo apt-get update && sudo apt-get install -y g++-arm-linux-gnueabihf
- name: Build (ARM 32-bit)
run: arm-linux-gnueabihf-g++ -std=c++11 -Wall -Wextra -Wno-psabi -Werror -c -o /dev/null test/test_32bit_build.cpp
+3 -7
View File
@@ -40,7 +40,7 @@ jobs:
clang-format --version
cd test && make style_check
build-error-check-on-32bit:
build-and-test-on-32bit:
runs-on: ubuntu-latest
if: >
(github.event_name == 'push') ||
@@ -64,9 +64,8 @@ jobs:
libssl-dev${{ matrix.config.arch_suffix }} libcurl4-openssl-dev${{ matrix.config.arch_suffix }} \
zlib1g-dev${{ matrix.config.arch_suffix }} libbrotli-dev${{ matrix.config.arch_suffix }} \
libzstd-dev${{ matrix.config.arch_suffix }}
- name: build and run tests (expect failure)
- name: build and run tests
run: cd test && make test EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}"
continue-on-error: true
ubuntu:
runs-on: ubuntu-latest
@@ -229,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" }
}
@@ -249,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"
+79
View File
@@ -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"
+5 -1
View File
@@ -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
+4
View File
@@ -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
-3
View File
@@ -173,9 +173,6 @@ if(CMAKE_SYSTEM_NAME MATCHES "Windows")
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})
+2 -2
View File
@@ -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()` |
+20
View File
@@ -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.
+36 -36
View File
@@ -5,11 +5,14 @@
A C++11 single-file header-only cross platform HTTP/HTTPS library.<br>
It's extremely easy to set up. Just include the **[httplib.h](https://raw.githubusercontent.com/yhirose/cpp-httplib/refs/heads/master/httplib.h)** file in your code!
**Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib/)**.
Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib/) (built with [docs-gen](https://github.com/yhirose/docs-gen)).
> [!IMPORTANT]
> This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want.
> [!WARNING]
> 32-bit platforms are **NOT supported**. Use at your own risk. The library may compile on 32-bit targets, but no security review has been conducted for 32-bit environments. Integer truncation and other 32-bit-specific issues may exist. **Security reports that only affect 32-bit platforms will be closed without action.** The maintainer does not have access to 32-bit environments for testing or fixing issues. CI includes basic compile checks only, not functional or security testing.
## Main Features
- HTTP Server/Client
@@ -61,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
@@ -194,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.
@@ -347,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
@@ -538,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;
}
@@ -583,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
+11 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.37.0"
version = "0.38.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
@@ -14,6 +14,16 @@ label = "Tour"
path = "tour/"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>'
[[nav]]
label = "Cookbook"
path = "cookbook/"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
[[nav]]
label = "LLM App"
path = "llm-app/"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
[[nav]]
label = "GitHub"
url = "https://github.com/yhirose/cpp-httplib"
+90 -3
View File
@@ -1,8 +1,95 @@
---
title: "Cookbook"
order: 1
order: 0
---
This section is under construction.
A collection of recipes that answer "How do I...?" questions. Each recipe is self-contained — read only what you need.
Check back soon for a collection of recipes organized by topic.
## Client
### Basics
- Get the response body as a string / save to a file
- Send and receive JSON
- Set default headers (`set_default_headers`)
- Follow redirects (`set_follow_location`)
### Authentication
- Use Basic authentication (`set_basic_auth`)
- Call an API with a Bearer token
### File Upload
- Upload a file as multipart form data (`make_file_provider`)
- POST a file as raw binary (`make_file_body`)
- Send the body with chunked transfer (Content Provider)
### Streaming & Progress
- Receive a response as a stream
- Use the progress callback (`DownloadProgress` / `UploadProgress`)
### Connection & Performance
- Set timeouts (`set_connection_timeout` / `set_read_timeout`)
- Set an overall timeout (`set_max_timeout`)
- Understand connection reuse and Keep-Alive behavior
- Enable compression (`set_compress` / `set_decompress`)
- Send requests through a proxy (`set_proxy`)
### Error Handling & Debugging
- Handle error codes (`Result::error()`)
- Handle SSL errors (`ssl_error()` / `ssl_backend_error()`)
- Set up client logging (`set_logger` / `set_error_logger`)
## Server
### Basics
- Register GET / POST / PUT / DELETE handlers
- Receive JSON requests and return JSON responses
- Use path parameters (`/users/:id`)
- Set up a static file server (`set_mount_point`)
### Streaming & Files
- Stream a large file in the response (`ContentProvider`)
- Return a file download response (`Content-Disposition`)
- Receive multipart data as a stream (`ContentReader`)
- Compress responses (gzip)
### Handler Chain
- Add pre-processing to all routes (Pre-routing handler)
- Add response headers with a Post-routing handler (CORS, etc.)
- Authenticate per route with a Pre-request handler (`matched_route`)
- Pass data between handlers with `res.user_data`
### Error Handling & Debugging
- Return custom error pages (`set_error_handler`)
- Catch exceptions (`set_exception_handler`)
- Log requests (Logger)
- Detect client disconnection (`req.is_connection_closed()`)
### Operations & Tuning
- Assign a port dynamically (`bind_to_any_port`)
- Control startup order with `listen_after_bind`
- Shut down gracefully (`stop()` and signal handling)
- Tune Keep-Alive (`set_keep_alive_max_count` / `set_keep_alive_timeout`)
- Configure the thread pool (`new_task_queue`)
## TLS / Security
- Choosing between OpenSSL, mbedTLS, and wolfSSL (build-time `#define` differences)
- Control SSL certificate verification (disable, custom CA, custom callback)
- Use a custom certificate verification callback (`set_server_certificate_verifier`)
- Set up an SSL/TLS server (certificate and private key)
- Configure mTLS (mutual TLS with client certificates)
- Access the peer certificate on the server (`req.peer_cert()` / SNI)
## SSE
- Implement an SSE server
- Use event names to distinguish event types
- Handle reconnection (`Last-Event-ID`)
- Receive SSE events on the client
## WebSocket
- Implement a WebSocket echo server and client
- Configure heartbeats (`set_websocket_ping_interval` / `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`)
- Handle connection close
- Send and receive binary frames
+1
View File
@@ -19,3 +19,4 @@ Under the hood, it uses blocking I/O with a thread pool. It's not built for hand
- [A Tour of cpp-httplib](tour/) — A step-by-step tutorial covering the basics. Start here if you're new
- [Cookbook](cookbook/) — A collection of recipes organized by topic. Jump to whatever you need
- [Building a Desktop LLM App](llm-app/) — A hands-on guide to building a desktop app with llama.cpp, step by step
+22
View File
@@ -0,0 +1,22 @@
---
title: "Building a Desktop LLM App with cpp-httplib"
order: 0
---
Build an LLM-powered translation desktop app step by step, learning both the server and client sides of cpp-httplib along the way. Translation is just an example — swap it out to build your own summarizer, code generator, chatbot, or any other LLM application.
## Dependencies
- [llama.cpp](https://github.com/ggml-org/llama.cpp) — LLM inference engine
- [nlohmann/json](https://github.com/nlohmann/json) — JSON parser (header-only)
- [webview/webview](https://github.com/webview/webview) — WebView wrapper (header-only)
- [cpp-httplib](https://github.com/yhirose/cpp-httplib) — HTTP server/client (header-only)
## Chapters
1. **Embed llama.cpp and create a REST API** — Start with a simple API that accepts text via POST and returns a translation as JSON
2. **Add token streaming with SSE** — Stream translation results token by token using the standard LLM API approach
3. **Add model discovery and download** — Use the client to search and download GGUF models from Hugging Face
4. **Add a Web UI** — Serve a translation UI with static file hosting, making the app accessible from a browser
5. **Turn it into a desktop app with WebView** — Wrap the web app with webview/webview to create an Electron-like desktop application
6. **Code reading: llama.cpp's server implementation** — Compare your implementation with production-quality code and learn from the differences
+4 -4
View File
@@ -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");
});
```
+1 -1
View File
@@ -1,6 +1,6 @@
---
title: "A Tour of cpp-httplib"
order: 1
order: 0
---
This is a step-by-step tutorial that walks you through the basics of cpp-httplib. Each chapter builds on the previous one, so please read them in order starting from Chapter 1.
+90 -3
View File
@@ -1,8 +1,95 @@
---
title: "Cookbook"
order: 1
order: 0
---
This section is under construction.
「〇〇をするには?」という問いに答えるレシピ集です。各レシピは独立しているので、必要なページだけ読めます。
Check back soon for a collection of recipes organized by topic.
## クライアント
### 基本
- レスポンスボディを文字列で取得する / ファイルに保存する
- JSON を送受信する
- デフォルトヘッダーを設定する(`set_default_headers`
- リダイレクトを追従する(`set_follow_location`
### 認証
- Basic 認証を使う(`set_basic_auth`
- Bearer トークンで API を呼ぶ
### ファイル送信
- ファイルをマルチパートフォームとしてアップロードする(`make_file_provider`
- ファイルを生バイナリとして POST する(`make_file_body`
- チャンク転送でボディを送る(Content Provider
### ストリーミング・進捗
- レスポンスをストリーミングで受信する
- 進捗コールバックを使う(`DownloadProgress` / `UploadProgress`
### 接続・パフォーマンス
- タイムアウトを設定する(`set_connection_timeout` / `set_read_timeout`
- 全体タイムアウトを設定する(`set_max_timeout`
- 接続の再利用と Keep-Alive の挙動を理解する
- 圧縮を有効にする(`set_compress` / `set_decompress`
- プロキシを経由してリクエストを送る(`set_proxy`
### エラー処理・デバッグ
- エラーコードをハンドリングする(`Result::error()`
- SSL エラーをハンドリングする(`ssl_error()` / `ssl_backend_error()`
- クライアントにログを設定する(`set_logger` / `set_error_logger`
## サーバー
### 基本
- GET / POST / PUT / DELETE ハンドラを登録する
- JSON リクエストを受け取り JSON レスポンスを返す
- パスパラメーターを使う(`/users/:id`
- 静的ファイルサーバーを設定する(`set_mount_point`
### ストリーミング・ファイル
- 大きなファイルをストリーミングで返す(`ContentProvider`
- ファイルダウンロードレスポンスを返す(`Content-Disposition`
- マルチパートデータをストリーミングで受け取る(`ContentReader`
- レスポンスを圧縮して返す(gzip)
### ハンドラチェーン
- 全ルートに共通の前処理をする(Pre-routing handler
- Post-routing handler でレスポンスヘッダーを追加する(CORS など)
- Pre-request handler でルート単位の認証を行う(`matched_route`
- `res.user_data` でハンドラ間データを渡す
### エラー処理・デバッグ
- カスタムエラーページを返す(`set_error_handler`
- 例外をキャッチする(`set_exception_handler`
- リクエストをログに記録する(Logger)
- クライアントが切断したか検出する(`req.is_connection_closed()`
### 運用・チューニング
- ポートを動的に割り当てる(`bind_to_any_port`
- `listen_after_bind` で起動順序を制御する
- グレースフルシャットダウンする(`stop()` とシグナルハンドリング)
- Keep-Alive を調整する(`set_keep_alive_max_count` / `set_keep_alive_timeout`
- マルチスレッド数を設定する(`new_task_queue`
## TLS / セキュリティ
- OpenSSL・mbedTLS・wolfSSL の選択指針(ビルド時の `#define` の違い)
- SSL 証明書の検証を制御する(証明書の無効化・カスタム CA・カスタムコールバック)
- カスタム証明書検証コールバックを使う(`set_server_certificate_verifier`
- SSL/TLS サーバーを立ち上げる(証明書・秘密鍵の設定)
- mTLS(クライアント証明書による相互認証)を設定する
- サーバー側でピア証明書を参照する(`req.peer_cert()` / SNI
## SSE
- SSE サーバーを実装する
- SSE でイベント名を使い分ける
- SSE の再接続を処理する(`Last-Event-ID`
- SSE をクライアントで受信する
## WebSocket
- WebSocket エコーサーバー/クライアントを実装する
- ハートビートを設定する(`set_websocket_ping_interval` / `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND`
- 接続クローズをハンドリングする
- バイナリフレームを送受信する
+1
View File
@@ -19,3 +19,4 @@ HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバ
- [A Tour of cpp-httplib](tour/) — 基本を順を追って学べるチュートリアル。初めての方はここから
- [Cookbook](cookbook/) — 目的別のレシピ集。必要なトピックから読めます
- [Building a Desktop LLM App](llm-app/) — llama.cpp を組み込んだデスクトップアプリを段階的に構築する実践ガイド
+22
View File
@@ -0,0 +1,22 @@
---
title: "Building a Desktop LLM App with cpp-httplib"
order: 0
---
llama.cpp を組み込んだ LLM 翻訳デスクトップアプリを段階的に構築しながら、cpp-httplib のサーバー・クライアント両面の使い方を実践的に学びます。翻訳は一例であり、この部分を差し替えることで要約・コード生成・チャットボットなど自分のアプリに応用できます。
## 依存ライブラリ
- [llama.cpp](https://github.com/ggml-org/llama.cpp) — LLM 推論エンジン
- [nlohmann/json](https://github.com/nlohmann/json) — JSON パーサー(ヘッダーオンリー)
- [webview/webview](https://github.com/webview/webview) — WebView ラッパー(ヘッダーオンリー)
- [cpp-httplib](https://github.com/yhirose/cpp-httplib) — HTTP サーバー/クライアント(ヘッダーオンリー)
## 章立て
1. **llama.cpp を組み込んで REST API を作る** — テキストを POST すると翻訳結果を JSON で返すシンプルな API から始める
2. **SSE でトークンストリーミングを追加する** — 翻訳結果をトークン単位で逐次返す LLM API 標準の方式を実装する
3. **モデルの取得・管理機能を追加する** — Hugging Face から GGUF モデルを検索・ダウンロードするクライアント機能を実装する
4. **Web UI を追加する** — 静的ファイル配信で翻訳 UI をホストし、ブラウザから操作できるようにする
5. **WebView でデスクトップアプリ化する** — webview/webview で包み、Electron 的なデスクトップアプリとして動作させる
6. **llama.cpp 本家のサーバー実装をコードリーディング** — 自分で作ったものとプロダクション品質のコードを比較して学ぶ
+4 -4
View File
@@ -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");
});
```
+1 -1
View File
@@ -1,6 +1,6 @@
---
title: "A Tour of cpp-httplib"
order: 1
order: 0
---
cpp-httplibの基本を、順番に学んでいくチュートリアルです。各章は前の章の内容を踏まえて進む構成なので、1章から順に読んでください。
+355 -167
View File
@@ -8,28 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.37.0"
#define CPPHTTPLIB_VERSION_NUM "0x002500"
/*
* Platform compatibility check
*/
#if defined(_WIN32) && !defined(_WIN64)
#if defined(_MSC_VER)
#pragma message( \
"cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler.")
#else
#warning \
"cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler."
#endif
#elif defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ < 8
#warning \
"cpp-httplib doesn't support 32-bit platforms. Please use a 64-bit compiler."
#elif defined(__SIZEOF_SIZE_T__) && __SIZEOF_SIZE_T__ < 8
#warning \
"cpp-httplib doesn't support platforms where size_t is less than 64 bits."
#endif
#define CPPHTTPLIB_VERSION "0.38.0"
#define CPPHTTPLIB_VERSION_NUM "0x002600"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -709,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 {
@@ -1678,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();
@@ -1712,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 =
@@ -1781,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_;
@@ -2198,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
@@ -2724,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,
@@ -2801,7 +2809,7 @@ inline size_t get_header_value_u64(const Headers &headers,
std::advance(it, static_cast<ssize_t>(id));
if (it != rng.second) {
if (is_numeric(it->second)) {
return std::strtoull(it->second.data(), nullptr, 10);
return static_cast<size_t>(std::strtoull(it->second.data(), nullptr, 10));
} else {
is_invalid_value = true;
}
@@ -2923,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 &params);
std::pair<std::string, std::string> make_range_header(const Ranges &ranges);
@@ -3726,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();
}
@@ -3745,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_;
@@ -3773,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);
@@ -3796,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;
@@ -4842,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());
@@ -5812,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));
@@ -6444,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,
@@ -6754,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;
@@ -6833,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;
}
@@ -7576,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;
@@ -7594,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
@@ -8241,7 +8353,8 @@ get_range_offset_and_length(Range r, size_t content_length) {
assert(r.first <= r.second &&
r.second < static_cast<ssize_t>(content_length));
(void)(content_length);
return std::make_pair(r.first, static_cast<size_t>(r.second - r.first) + 1);
return std::make_pair(static_cast<size_t>(r.first),
static_cast<size_t>(r.second - r.first) + 1);
}
inline std::string make_content_range_header_field(
@@ -9287,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); }
@@ -9392,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 &params) {
std::string path_with_query = path;
@@ -10520,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;
}
}
@@ -10680,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);
@@ -11100,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()) {
@@ -11338,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);
@@ -11814,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;
@@ -12058,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
@@ -12179,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
@@ -12360,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);
@@ -12433,11 +12604,17 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
handle.body_reader_.stream = handle.stream_;
handle.body_reader_.payload_max_length = payload_max_length_;
auto content_length_str = handle.response->get_header_value("Content-Length");
if (!content_length_str.empty()) {
if (handle.response->has_header("Content-Length")) {
bool is_invalid = false;
auto content_length = detail::get_header_value_u64(
handle.response->headers, "Content-Length", 0, 0, is_invalid);
if (is_invalid) {
handle.error = Error::Read;
handle.response.reset();
return handle;
}
handle.body_reader_.has_content_length = true;
handle.body_reader_.content_length =
static_cast<size_t>(std::stoull(content_length_str));
handle.body_reader_.content_length = content_length;
}
auto transfer_encoding =
@@ -12643,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_
@@ -12721,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;
}
@@ -12772,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()) {
@@ -12890,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>();
}
@@ -13128,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;
@@ -13146,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);
@@ -13170,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);
@@ -14500,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
@@ -15348,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(
@@ -19864,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)) {
@@ -19913,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();
@@ -20005,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;
}
@@ -20045,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) {
+1
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
services:
squid_basic:
extra_hosts:
- "host.docker.internal:host-gateway"
squid_digest:
extra_hosts:
- "host.docker.internal:host-gateway"
+411 -187
View File
@@ -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;
@@ -13712,6 +13827,102 @@ TEST_F(OpenStreamTest, ProhibitedTrailersAreIgnored_Stream) {
EXPECT_EQ(std::string(""), handle.response->get_header_value("X-Allowed"));
}
static std::thread serve_single_response(int port,
const std::string &response) {
return std::thread([port, response] {
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
default_socket_options(srv);
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_SNDTIMEO, 5, 0);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(port));
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
int opt = 1;
::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR,
#ifdef _WIN32
reinterpret_cast<const char *>(&opt),
#else
&opt,
#endif
sizeof(opt));
::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
::listen(srv, 1);
sockaddr_in cli_addr{};
socklen_t cli_len = sizeof(cli_addr);
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
if (cli != INVALID_SOCKET) {
char buf[4096];
::recv(cli, buf, sizeof(buf), 0);
::send(cli,
#ifdef _WIN32
static_cast<const char *>(response.c_str()),
static_cast<int>(response.size()),
#else
response.c_str(), response.size(),
#endif
0);
detail::close_socket(cli);
}
detail::close_socket(srv);
});
}
TEST(OpenStreamMalformedContentLength, InvalidArgument) {
#ifndef _WIN32
signal(SIGPIPE, SIG_IGN);
#endif
auto server_thread =
serve_single_response(PORT + 2, "HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: not-a-number\r\n"
"Connection: close\r\n"
"\r\n"
"hello");
std::this_thread::sleep_for(std::chrono::milliseconds(200));
Client cli("127.0.0.1", PORT + 2);
auto handle = cli.open_stream("GET", "/");
EXPECT_FALSE(handle.is_valid());
server_thread.join();
}
TEST(OpenStreamMalformedContentLength, OutOfRange) {
#ifndef _WIN32
signal(SIGPIPE, SIG_IGN);
#endif
auto server_thread = serve_single_response(
PORT + 4, "HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 99999999999999999999999999\r\n"
"Connection: close\r\n"
"\r\n"
"hello");
std::this_thread::sleep_for(std::chrono::milliseconds(200));
// Before the fix, std::stoull would throw std::out_of_range here and
// 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 + 4);
auto handle = cli.open_stream("GET", "/");
EXPECT_TRUE(handle.is_valid());
server_thread.join();
}
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
TEST_F(OpenStreamTest, Gzip) {
Client cli("127.0.0.1", port_);
@@ -15940,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
// ============================================================================
@@ -16329,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) {
@@ -16843,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
+9
View File
@@ -0,0 +1,9 @@
#include "../httplib.h"
int main() {
httplib::Server svr;
httplib::Client cli("localhost", 8080);
(void)svr;
(void)cli;
return 0;
}
+78
View File
@@ -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"); }
+71
View File
@@ -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
+79
View File
@@ -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");