mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
30 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 | |||
| 68fa9bce0f | |||
| 1f34c541b0 | |||
| e41ec36274 | |||
| 7489fd3a8b | |||
| ab3098f18b |
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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`)
|
||||
- 接続クローズをハンドリングする
|
||||
- バイナリフレームを送受信する
|
||||
|
||||
@@ -19,3 +19,4 @@ HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバ
|
||||
|
||||
- [A Tour of cpp-httplib](tour/) — 基本を順を追って学べるチュートリアル。初めての方はここから
|
||||
- [Cookbook](cookbook/) — 目的別のレシピ集。必要なトピックから読めます
|
||||
- [Building a Desktop LLM App](llm-app/) — llama.cpp を組み込んだデスクトップアプリを段階的に構築する実践ガイド
|
||||
|
||||
@@ -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 本家のサーバー実装をコードリーディング** — 自分で作ったものとプロダクション品質のコードを比較して学ぶ
|
||||
@@ -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,6 +1,6 @@
|
||||
---
|
||||
title: "A Tour of cpp-httplib"
|
||||
order: 1
|
||||
order: 0
|
||||
---
|
||||
|
||||
cpp-httplibの基本を、順番に学んでいくチュートリアルです。各章は前の章の内容を踏まえて進む構成なので、1章から順に読んでください。
|
||||
|
||||
@@ -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 ¶ms);
|
||||
|
||||
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 ¶ms) {
|
||||
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) {
|
||||
|
||||
@@ -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"
|
||||
+411
-187
@@ -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
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "../httplib.h"
|
||||
|
||||
int main() {
|
||||
httplib::Server svr;
|
||||
httplib::Client cli("localhost", 8080);
|
||||
(void)svr;
|
||||
(void)cli;
|
||||
return 0;
|
||||
}
|
||||
@@ -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