mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec5ce17929 | |||
| f6524c0802 | |||
| 35c4026c7f | |||
| 40e18460bc | |||
| 92aecf85d8 | |||
| b223e29778 | |||
| 2d2efe46da | |||
| cae753425e | |||
| d412e98c62 | |||
| 806fcb8268 | |||
| c2678f0186 | |||
| 0cbeafe6a4 | |||
| 13e866bdb0 | |||
| db6c9ef27b | |||
| 887837c65b | |||
| 3d56762d5c | |||
| 109e331068 | |||
| 2ea632264d | |||
| 511cc02278 | |||
| f50bd311fb | |||
| b0866cff8f | |||
| 5ebbfeef0b | |||
| d14e4fc05f | |||
| 33bc1df930 | |||
| 02d3825149 | |||
| 9f41fc0447 | |||
| 3cedf31d4c | |||
| cc8f270d4b | |||
| 9f52821be6 | |||
| b045ee7f6b | |||
| cb3fce964d | |||
| 7e2a173072 | |||
| ee5d15c842 | |||
| 09d00c099c | |||
| 6bdd657713 | |||
| b4eec3ee77 | |||
| c0248ff7fc | |||
| 203e1bf2ac |
+106
-6
@@ -25,7 +25,9 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '*' }}
|
||||
# Exclude *_Online tests by default — they hit external services and flake on
|
||||
# CI runners. Run with workflow_dispatch + a custom filter to include them.
|
||||
GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '-*_Online' }}
|
||||
|
||||
jobs:
|
||||
style-check:
|
||||
@@ -75,6 +77,7 @@ jobs:
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tls_backend: [openssl, mbedtls, wolfssl]
|
||||
name: ubuntu (${{ matrix.tls_backend }})
|
||||
@@ -101,7 +104,10 @@ jobs:
|
||||
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
|
||||
- name: build and run tests (Mbed TLS)
|
||||
if: matrix.tls_backend == 'mbedtls'
|
||||
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
|
||||
# Run mbedTLS shards with reduced parallelism — under ASAN+mbedTLS the
|
||||
# default 4 shards overload CI runners enough that timing-sensitive
|
||||
# ServerTest cases flake on first-request keep-alive reuse.
|
||||
run: cd test && make test_split_mbedtls && SHARDS=2 make test_mbedtls_parallel
|
||||
- name: build and run tests (wolfSSL)
|
||||
if: matrix.tls_backend == 'wolfssl'
|
||||
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
|
||||
@@ -114,6 +120,92 @@ jobs:
|
||||
- name: build and run ThreadPool test
|
||||
run: cd test && make test_thread_pool && ./test_thread_pool
|
||||
|
||||
# Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
|
||||
# On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous
|
||||
# DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb.
|
||||
# When gai_suspend() hits the connection timeout, gai_cancel() is called
|
||||
# but does not block; the resolver worker can later write back into the
|
||||
# destroyed stack frame. To make the worker actually reach that write,
|
||||
# the test job runs a loopback UDP responder (test/dns_test_fixture.py)
|
||||
# that delays its reply past the test's 1s timeout, and uses an iptables
|
||||
# NAT rule so glibc's lookups land on that fixture instead of a real
|
||||
# nameserver. With ASAN's detect_stack_use_after_return enabled, the
|
||||
# late write-back is reported as a stack-use-after-return.
|
||||
issue-2431-repro:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
(github.event_name == 'push') ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
|
||||
name: issue-2431 repro (Linux + ASAN)
|
||||
# Bound the whole job in case anything in the test harness hangs
|
||||
# unexpectedly. With the fixture in place a normal run is well under
|
||||
# a minute either way (ASAN abort on broken HEAD, clean pass on fix).
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
DNS_FIXTURE_PORT: "15353"
|
||||
DNS_FIXTURE_DELAY: "3"
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: install libraries
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libssl-dev zlib1g-dev libbrotli-dev \
|
||||
libzstd-dev libcurl4-openssl-dev iptables util-linux iproute2
|
||||
- name: start loopback DNS test fixture
|
||||
run: |
|
||||
# Force glibc through its DNS code path: Ubuntu's default
|
||||
# nsswitch short-circuits to NOTFOUND through mdns4_minimal,
|
||||
# which would skip the buggy code entirely.
|
||||
sudo sed -i 's/^hosts:.*/hosts: dns/' /etc/nsswitch.conf
|
||||
# Run the loopback fixture (delayed UDP responder).
|
||||
python3 test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \
|
||||
>/tmp/dns_fixture.log 2>&1 &
|
||||
echo $! | sudo tee /tmp/dns_fixture.pid >/dev/null
|
||||
# Wait for the fixture to start listening.
|
||||
for _ in $(seq 1 50); do
|
||||
if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" \
|
||||
|| { echo "fixture failed to start"; cat /tmp/dns_fixture.log; exit 1; }
|
||||
# Send the test process's DNS lookups to the loopback fixture.
|
||||
# NAT only the local OUTPUT chain; conntrack handles the reply path.
|
||||
sudo iptables -t nat -I OUTPUT -p udp --dport 53 \
|
||||
-j REDIRECT --to-port "$DNS_FIXTURE_PORT"
|
||||
# Sanity check: a query must take at least the fixture delay
|
||||
# and resolve to NXDOMAIN (proving traffic reaches the fixture).
|
||||
start=$(date +%s)
|
||||
getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
if [ "$elapsed" -lt 2 ]; then
|
||||
echo "ERROR: lookup returned in ${elapsed}s; fixture not in path" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)"
|
||||
- name: build test binary
|
||||
run: cd test && make test
|
||||
- name: run GetAddrInfoAsyncCancelTest
|
||||
run: |
|
||||
cd test
|
||||
ARCH=$(uname -m)
|
||||
CPPHTTPLIB_TEST_ISSUE_2431=1 \
|
||||
ASAN_OPTIONS=detect_stack_use_after_return=1 \
|
||||
LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
|
||||
setarch "$ARCH" -R \
|
||||
./test --gtest_filter='GetAddrInfoAsyncCancelTest.*'
|
||||
- name: tear down test fixture
|
||||
if: always()
|
||||
run: |
|
||||
sudo iptables -t nat -F OUTPUT || true
|
||||
if [ -f /tmp/dns_fixture.pid ]; then
|
||||
sudo kill "$(cat /tmp/dns_fixture.pid)" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
if: >
|
||||
@@ -122,6 +214,7 @@ jobs:
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tls_backend: [openssl, mbedtls, wolfssl]
|
||||
name: macos (${{ matrix.tls_backend }})
|
||||
@@ -141,7 +234,10 @@ jobs:
|
||||
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
|
||||
- name: build and run tests (Mbed TLS)
|
||||
if: matrix.tls_backend == 'mbedtls'
|
||||
run: cd test && make test_split_mbedtls && make test_mbedtls_parallel
|
||||
# macOS runners under ASAN+mbedTLS still flake at SHARDS=2 (rapid
|
||||
# bind/connect on the fixture's fixed port races on the slower
|
||||
# macos-latest runner). Serialize fully here; ubuntu stays at 2.
|
||||
run: cd test && make test_split_mbedtls && SHARDS=1 make test_mbedtls_parallel
|
||||
- name: build and run tests (wolfSSL)
|
||||
if: matrix.tls_backend == 'wolfssl'
|
||||
run: cd test && make test_split_wolfssl && make test_wolfssl_parallel
|
||||
@@ -162,6 +258,7 @@ jobs:
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_windows == 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- with_ssl: false
|
||||
@@ -228,7 +325,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 || '*' }}" `
|
||||
-ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '-*_Online' }}" `
|
||||
-NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" `
|
||||
-Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" }
|
||||
}
|
||||
@@ -236,11 +333,14 @@ jobs:
|
||||
$failed = $false
|
||||
for ($i = 0; $i -lt $shards; $i++) {
|
||||
$log = "shard_${i}.log"
|
||||
if (Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet) {
|
||||
$proc = $procs[$i]
|
||||
$hasPassed = Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet
|
||||
$hasFailed = Select-String -Path $log -Pattern "\[ FAILED \]" -Quiet
|
||||
if ($hasPassed -and -not $hasFailed -and $proc.ExitCode -eq 0) {
|
||||
$passed = (Select-String -Path $log -Pattern "\[ PASSED \]").Line
|
||||
Write-Host "Shard ${i}: $passed"
|
||||
} else {
|
||||
Write-Host "=== Shard $i FAILED ==="
|
||||
Write-Host "=== Shard $i FAILED (exit=$($proc.ExitCode)) ==="
|
||||
Get-Content $log
|
||||
if (Test-Path "${log}.err") { Get-Content "${log}.err" }
|
||||
$failed = $true
|
||||
|
||||
@@ -43,6 +43,9 @@ test/test_mbedtls
|
||||
test/test_wolfssl
|
||||
test/test_no_tls
|
||||
test/server_fuzzer
|
||||
test/client_fuzzer
|
||||
test/header_parser_fuzzer
|
||||
test/url_parser_fuzzer
|
||||
test/test_proxy
|
||||
test/test_proxy_mbedtls
|
||||
test/test_proxy_wolfssl
|
||||
|
||||
+21
-2
@@ -3,15 +3,19 @@
|
||||
A simple, blocking WebSocket implementation for C++11.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This is a blocking I/O WebSocket implementation using a thread-per-connection model. If you need high-concurrency WebSocket support with non-blocking/async I/O (e.g., thousands of simultaneous connections), this is not the one that you want.
|
||||
> This is a blocking I/O WebSocket implementation using a thread-per-connection model (plus one heartbeat thread per connection). It is intended for small- to mid-scale workloads; handling large numbers of simultaneous WebSocket connections is outside the design target of this library. If you need high-concurrency WebSocket support with non-blocking/async I/O (e.g., thousands of simultaneous connections), this is not the one that you want.
|
||||
|
||||
> [!NOTE]
|
||||
> WebSocket extensions (`permessage-deflate` and others defined by RFC 6455) are **not supported**. If a client proposes an extension via `Sec-WebSocket-Extensions`, the server silently declines it — the negotiated connection always runs without extensions.
|
||||
|
||||
## Features
|
||||
|
||||
- **RFC 6455 compliant**: Full WebSocket protocol support
|
||||
- **RFC 6455 compliant**: Full WebSocket protocol support (extensions are not implemented)
|
||||
- **Server and Client**: Both sides included
|
||||
- **SSL/TLS support**: `wss://` scheme for secure connections
|
||||
- **Text and Binary**: Both message types supported
|
||||
- **Automatic heartbeat**: Periodic Ping/Pong keeps connections alive
|
||||
- **Unresponsive-peer detection**: Opt-in liveness check via `set_websocket_max_missed_pongs()`
|
||||
- **Subprotocol negotiation**: `Sec-WebSocket-Protocol` support for GraphQL, MQTT, etc.
|
||||
|
||||
## Quick Start
|
||||
@@ -352,6 +356,7 @@ if (ws.connect()) {
|
||||
| `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` | `300` | Read timeout for WebSocket connections (seconds) |
|
||||
| `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) |
|
||||
| `CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS` | `0` (disabled) | Close the connection after N consecutive unacked pings |
|
||||
|
||||
### Runtime Ping Interval
|
||||
|
||||
@@ -373,6 +378,20 @@ ws.set_websocket_ping_interval(10); // 10 seconds
|
||||
ws.set_websocket_ping_interval(0);
|
||||
```
|
||||
|
||||
### Unresponsive-Peer Detection (Pong Timeout)
|
||||
|
||||
By default the heartbeat only sends pings — it does not enforce that pongs come back. To detect a silently dropped connection faster, enable the max-missed-pongs check. Once `max_missed_pongs` consecutive pings go unanswered, the heartbeat thread closes the connection with `CloseStatus::GoingAway` and the reason `"pong timeout"`.
|
||||
|
||||
```cpp
|
||||
ws.set_websocket_max_missed_pongs(2); // close after 2 consecutive unacked pings
|
||||
```
|
||||
|
||||
The server side has the same `set_websocket_max_missed_pongs()`.
|
||||
|
||||
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer within ~60 seconds. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()` — exactly the pattern a normal WebSocket client already uses.
|
||||
|
||||
**The default is `0`**, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses are not checked. Even so, a dead connection does not linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. `max_missed_pongs` is the knob for detecting an unresponsive peer faster than that 5-minute fallback.
|
||||
|
||||
## Threading Model
|
||||
|
||||
WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime.
|
||||
|
||||
@@ -8,7 +8,7 @@ It's extremely easy to set up. Just include the **[httplib.h](https://raw.github
|
||||
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.
|
||||
> 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. Only **HTTP/1.1** is supported — HTTP/2 and HTTP/3 are not implemented.
|
||||
|
||||
> [!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.
|
||||
@@ -1462,7 +1462,11 @@ if (ws.connect()) {
|
||||
|
||||
SSL is also supported via `wss://` scheme (e.g. `WebSocketClient("wss://example.com/ws")`). Subprotocol negotiation (`Sec-WebSocket-Protocol`) is supported via `SubProtocolSelector` callback.
|
||||
|
||||
> **Note:** WebSocket connections occupy a thread for their entire lifetime. If you plan to handle many simultaneous WebSocket connections, consider using a dynamic thread pool: `svr.new_task_queue = [] { return new ThreadPool(8, 64); };`
|
||||
> **Note:** WebSocket connections occupy a thread for their entire lifetime (plus an additional thread per connection for heartbeat pings). This thread-per-connection model is intended for small- to mid-scale workloads; large numbers of simultaneous WebSocket connections are outside the design target of this library. If you expect many concurrent WebSocket clients, configure a dynamic thread pool (`svr.new_task_queue = [] { return new ThreadPool(8, 64); };`) and measure carefully.
|
||||
|
||||
> **WebSocket extensions are not supported.** `permessage-deflate` and other RFC 6455 extensions are not implemented. If a client proposes them via `Sec-WebSocket-Extensions`, the server silently declines them in its handshake response.
|
||||
|
||||
> **Unresponsive-peer detection.** Heartbeat pings also serve as a liveness probe when `set_websocket_max_missed_pongs(n)` is set: if the client sends `n` consecutive pings without receiving a pong, it will close the connection. Disabled by default (`0`).
|
||||
|
||||
See [README-websocket.md](README-websocket.md) for more details.
|
||||
|
||||
@@ -1564,10 +1568,6 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
|
||||
> [!NOTE]
|
||||
> cpp-httplib officially supports only the latest Visual Studio. It might work with former versions of Visual Studio, but I can no longer verify it. Pull requests are always welcome for the older versions of Visual Studio unless they break the C++11 conformance.
|
||||
|
||||
### Deprecated APIs
|
||||
|
||||
A handful of older APIs are marked `[[deprecated]]` and are scheduled to be removed **by v1.0.0**. If you see a deprecation warning at build time, please migrate to the replacement shown in the message — for example, `Client::set_url_encode()` → `set_path_encode()`, `SSLClient::set_ca_cert_store()` → `load_ca_cert_store()`, and `Result::ssl_openssl_error()` → `ssl_backend_error()`. The full list lives alongside the `[[deprecated]]` attributes in `httplib.h`.
|
||||
|
||||
> [!NOTE]
|
||||
> Windows 8 or lower, Visual Studio 2015 or lower, and Cygwin and MSYS2 including MinGW are neither supported nor tested.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.42.0"
|
||||
version = "0.43.3"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -57,4 +57,4 @@ Return `false` from the callback to abort the download. In the example above, if
|
||||
>
|
||||
> The `ResponseHandler` is called after headers arrive but before the body. Return `false` to skip the download entirely.
|
||||
|
||||
> To show download progress, see C11. Use the progress callback.
|
||||
> To show download progress, see [C11. Use the progress callback](c11-progress-callback).
|
||||
|
||||
@@ -31,6 +31,6 @@ if (res && res->status == 200) {
|
||||
|
||||
`res->body` is a `std::string`, so you can pass it straight to your JSON library.
|
||||
|
||||
> **Note:** Servers sometimes return HTML on errors. Check the status code before parsing to be safe. Some APIs also require an `Accept: application/json` header. If you're calling a JSON API repeatedly, C03. Set default headers can save you some boilerplate.
|
||||
> **Note:** Servers sometimes return HTML on errors. Check the status code before parsing to be safe. Some APIs also require an `Accept: application/json` header. If you're calling a JSON API repeatedly, [C03. Set default headers](c03-default-headers) can save you some boilerplate.
|
||||
|
||||
> For how to receive and return JSON on the server side, see S02. Receive JSON requests and return JSON responses.
|
||||
> For how to receive and return JSON on the server side, see [S02. Receive JSON requests and return JSON responses](s02-json-api).
|
||||
|
||||
@@ -52,4 +52,4 @@ auto res = cli.Get("/users", headers);
|
||||
|
||||
Per-request headers are **added** on top of the defaults. Both are sent to the server.
|
||||
|
||||
> For details on Bearer token auth, see C06. Call an API with a Bearer Token.
|
||||
> For details on Bearer token auth, see [C06. Call an API with a Bearer token](c06-bearer-token).
|
||||
|
||||
@@ -35,4 +35,4 @@ Many sites redirect HTTP traffic to HTTPS. With `set_follow_location(true)` on,
|
||||
|
||||
> **Warning:** To follow redirects to HTTPS, you need to build cpp-httplib with OpenSSL (or another TLS backend). Without TLS support, redirects to HTTPS will fail.
|
||||
|
||||
> **Note:** Following redirects adds to the total request time. See C12. Set Timeouts for timeout configuration.
|
||||
> **Note:** Following redirects adds to the total request time. See [C12. Set timeouts](c12-timeouts) for timeout configuration.
|
||||
|
||||
@@ -43,4 +43,4 @@ For the more secure Digest authentication scheme, use `set_digest_auth()`. This
|
||||
cli.set_digest_auth("alice", "s3cret");
|
||||
```
|
||||
|
||||
> To call an API with a Bearer token, see C06. Call an API with a Bearer Token.
|
||||
> To call an API with a Bearer token, see [C06. Call an API with a Bearer token](c06-bearer-token).
|
||||
|
||||
@@ -47,4 +47,4 @@ if (res && res->status == 401) {
|
||||
|
||||
> **Warning:** A Bearer token is itself a credential. Always send it over HTTPS, and never hard-code it into source or config files.
|
||||
|
||||
> To set multiple headers at once, see C03. Set Default Headers.
|
||||
> To set multiple headers at once, see [C03. Set default headers](c03-default-headers).
|
||||
|
||||
@@ -49,4 +49,4 @@ The arguments to `make_file_provider()` are `(form name, file path, file name, c
|
||||
|
||||
> **Note:** You can mix `UploadFormDataItems` and `FormDataProviderItems` in the same request. A clean split is: text fields in `UploadFormDataItems`, files in `FormDataProviderItems`.
|
||||
|
||||
> To show upload progress, see C11. Use the progress callback.
|
||||
> To show upload progress, see [C11. Use the progress callback](c11-progress-callback).
|
||||
|
||||
@@ -31,4 +31,4 @@ If the file can't be opened, `make_file_body()` returns `size` as `0` and `provi
|
||||
|
||||
> **Warning:** `make_file_body()` needs to fix the Content-Length up front, so it reads the file size ahead of time. If the file size might change mid-upload, this API isn't the right fit.
|
||||
|
||||
> To send the file as multipart form data instead, see C07. Upload a File as Multipart Form Data.
|
||||
> To send the file as multipart form data instead, see [C07. Upload a file as multipart form data](c07-multipart-upload).
|
||||
|
||||
@@ -44,4 +44,4 @@ With a known size, the request carries a Content-Length header — so the server
|
||||
|
||||
> **Detail:** `sink.write()` returns a `bool` indicating whether the write succeeded. If it returns `false`, the connection is gone — return `false` from the lambda to stop.
|
||||
|
||||
> If you're just sending a file, `make_file_body()` is easier. See C08. POST a File as Raw Binary.
|
||||
> If you're just sending a file, `make_file_body()` is easier. See [C08. POST a file as raw binary](c08-post-file-body).
|
||||
|
||||
@@ -48,5 +48,5 @@ Accumulate into a buffer, then pull out and parse one line each time you see a n
|
||||
|
||||
> **Warning:** When you pass a `ContentReceiver`, `res->body` stays **empty**. Store or process the body inside the callback yourself.
|
||||
|
||||
> To track download progress, combine this with C11. Use the Progress Callback.
|
||||
> For Server-Sent Events (SSE), see E04. Receive SSE on the Client.
|
||||
> To track download progress, combine this with [C11. Use the progress callback](c11-progress-callback).
|
||||
> For Server-Sent Events (SSE), see [E04. Receive SSE on the client](e04-sse-client).
|
||||
|
||||
@@ -56,4 +56,4 @@ auto res = cli.Get("/large-file",
|
||||
|
||||
> **Note:** `ContentReceiver` and the progress callback can be used together. When you want to stream to a file and show progress at the same time, pass both.
|
||||
|
||||
> For a concrete example of saving to a file, see C01. Get the Response Body / Save to a File.
|
||||
> For a concrete example of saving to a file, see [C01. Get the response body / save to a file](c01-get-response-body).
|
||||
|
||||
@@ -47,4 +47,4 @@ cli.set_connection_timeout(3s);
|
||||
cli.set_read_timeout(10s);
|
||||
```
|
||||
|
||||
> **Warning:** The read timeout covers a single receive call — not the whole request. If data keeps trickling in during a large download, the request can take half an hour without ever hitting the timeout. To cap the total request time, use C13. Set an Overall Timeout.
|
||||
> **Warning:** The read timeout covers a single receive call — not the whole request. If data keeps trickling in during a large download, the request can take half an hour without ever hitting the timeout. To cap the total request time, use [C13. Set an overall timeout](c13-max-timeout).
|
||||
|
||||
@@ -4,7 +4,7 @@ order: 13
|
||||
status: "draft"
|
||||
---
|
||||
|
||||
The three timeouts from C12. Set Timeouts all apply to a single `send` or `recv` call. To cap the total time a request can take, use `set_max_timeout()`.
|
||||
The three timeouts from [C12. Set timeouts](c12-timeouts) all apply to a single `send` or `recv` call. To cap the total time a request can take, use `set_max_timeout()`.
|
||||
|
||||
## Basic usage
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ cli.set_proxy_digest_auth("user", "password");
|
||||
|
||||
## Combine with end-server authentication
|
||||
|
||||
Proxy authentication is separate from authenticating to the end server (C05, C06). When both are needed, set both.
|
||||
Proxy authentication is separate from authenticating to the end server ([C05. Use Basic authentication](c05-basic-auth), [C06. Call an API with a Bearer token](c06-bearer-token)). When both are needed, set both.
|
||||
|
||||
```cpp
|
||||
cli.set_proxy("proxy.internal", 8080);
|
||||
|
||||
@@ -60,4 +60,4 @@ std::cout << res->body << std::endl;
|
||||
|
||||
Keep them separated in your head: network-layer errors go through `res.error()`, HTTP-level errors through `res->status`.
|
||||
|
||||
> To dig deeper into SSL-related errors, see C18. Handle SSL Errors.
|
||||
> To dig deeper into SSL-related errors, see [C18. Handle SSL errors](c18-ssl-errors).
|
||||
|
||||
@@ -48,6 +48,4 @@ if (res.ssl_backend_error() != 0) {
|
||||
| `SSLServerHostnameVerification` | The cert's CN/SAN doesn't match the host |
|
||||
| `SSLConnection` | TLS version mismatch, no shared cipher suite |
|
||||
|
||||
> **Note:** `ssl_backend_error()` was previously called `ssl_openssl_error()`. The old name is deprecated — use `ssl_backend_error()` now.
|
||||
|
||||
> To change certificate verification settings, see T02. Control SSL Certificate Verification.
|
||||
> To change certificate verification settings, see [T02. Control SSL certificate verification](t02-cert-verification).
|
||||
|
||||
@@ -56,7 +56,7 @@ svr.Get("/time", [](const httplib::Request &req, httplib::Response &res) {
|
||||
});
|
||||
```
|
||||
|
||||
When the client disconnects, call `sink.done()` to stop. Details in S16. Detect When the Client Has Disconnected.
|
||||
When the client disconnects, call `sink.done()` to stop. Details in [S16. Detect client disconnection](s16-disconnect).
|
||||
|
||||
## Heartbeats via comment lines
|
||||
|
||||
@@ -80,8 +80,8 @@ svr.new_task_queue = [] {
|
||||
};
|
||||
```
|
||||
|
||||
See S21. Configure the Thread Pool.
|
||||
See [S21. Configure the thread pool](s21-thread-pool).
|
||||
|
||||
> **Note:** When `data:` contains newlines, split it into multiple `data:` lines — one per line. This is how the SSE spec requires multiline data to be transmitted.
|
||||
|
||||
> For event names, see E02. Use Named Events in SSE. For the client side, see E04. Receive SSE on the Client.
|
||||
> For event names, see [E02. Use named events in SSE](e02-sse-event-names). For the client side, see [E04. Receive SSE on the client](e04-sse-client).
|
||||
|
||||
@@ -52,7 +52,7 @@ auto send_event = [](httplib::DataSink &sink,
|
||||
send_event(sink, "message", "Hello!", "42");
|
||||
```
|
||||
|
||||
The ID format is up to you. Monotonic counters or UUIDs both work — just pick something unique and orderable on the server side. See E03. Handle SSE Reconnection for details.
|
||||
The ID format is up to you. Monotonic counters or UUIDs both work — just pick something unique and orderable on the server side. See [E03. Handle SSE reconnection](e03-sse-reconnect) for details.
|
||||
|
||||
## JSON payloads in data
|
||||
|
||||
|
||||
@@ -96,4 +96,4 @@ Use `last_event_id()` to read the current value.
|
||||
|
||||
> **Note:** `SSEClient::start()` blocks, which is fine for a one-off command-line tool. For GUI apps or embedded in a server, the `start_async()` + `stop()` pair is the usual pattern.
|
||||
|
||||
> For the server side, see E01. Implement an SSE Server.
|
||||
> For the server side, see [E01. Implement an SSE server](e01-sse-server).
|
||||
|
||||
@@ -61,6 +61,6 @@ svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
|
||||
|
||||
To add a response header, use `res.set_header("Name", "Value")`.
|
||||
|
||||
> **Note:** `listen()` is a blocking call. To run it on a different thread, wrap it in `std::thread`. If you need non-blocking startup, see S18. Control Startup Order with `listen_after_bind`.
|
||||
> **Note:** `listen()` is a blocking call. To run it on a different thread, wrap it in `std::thread`. If you need non-blocking startup, see [S18. Control startup order with `listen_after_bind`](s18-listen-after-bind).
|
||||
|
||||
> To use path parameters like `/users/:id`, see S03. Use Path Parameters.
|
||||
> To use path parameters like `/users/:id`, see [S03. Use path parameters](s03-path-params).
|
||||
|
||||
@@ -69,6 +69,6 @@ svr.Get("/api/health", [&](const auto &req, auto &res) {
|
||||
});
|
||||
```
|
||||
|
||||
> **Note:** A large JSON body ends up entirely in `req.body`, which means it all sits in memory. For huge payloads, consider streaming reception — see S07. Receive Multipart Data as a Stream.
|
||||
> **Note:** A large JSON body ends up entirely in `req.body`, which means it all sits in memory. For huge payloads, consider streaming reception — see [S07. Receive multipart data as a stream](s07-multipart-reader).
|
||||
|
||||
> For the client side, see C02. Send and Receive JSON.
|
||||
> For the client side, see [C02. Send and receive JSON](c02-json).
|
||||
|
||||
@@ -52,4 +52,4 @@ svr.set_file_extension_and_mimetype_mapping("wasm", "application/wasm");
|
||||
|
||||
> **Warning:** The static file server methods are **not thread-safe**. Don't call them after `listen()` — configure everything before starting the server.
|
||||
|
||||
> For download-style responses, see S06. Return a File Download Response.
|
||||
> For download-style responses, see [S06. Return a file download response](s06-download-response).
|
||||
|
||||
@@ -60,4 +60,4 @@ Call `sink.done()` to signal the end.
|
||||
|
||||
> **Note:** The provider lambda is called multiple times. Watch out for the lifetime of captured variables — wrap them in a `std::shared_ptr` if needed.
|
||||
|
||||
> To serve the file as a download, see S06. Return a File Download Response.
|
||||
> To serve the file as a download, see [S06. Return a file download response](s06-download-response).
|
||||
|
||||
@@ -68,4 +68,4 @@ Only a small chunk sits in memory at any moment, so gigabyte-scale files are no
|
||||
|
||||
> **Warning:** When you use `HandlerWithContentReader`, `req.body` stays **empty**. Handle the body yourself inside the callbacks.
|
||||
|
||||
> For the client side of multipart uploads, see C07. Upload a File as Multipart Form Data.
|
||||
> For the client side of multipart uploads, see [C07. Upload a file as multipart form data](c07-multipart-upload).
|
||||
|
||||
@@ -50,4 +50,4 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
|
||||
|
||||
> **Note:** Tiny responses barely benefit from compression and just waste CPU time. cpp-httplib skips compression for bodies that are too small to bother with.
|
||||
|
||||
> For the client-side counterpart, see C15. Enable Compression.
|
||||
> For the client-side counterpart, see [C15. Enable compression](c15-compression).
|
||||
|
||||
@@ -49,6 +49,6 @@ If auth fails, return `Handled` to respond with 401 immediately. If it passes, r
|
||||
|
||||
## For per-route auth
|
||||
|
||||
If you want different auth rules per route rather than a single global check, `set_pre_request_handler()` is a better fit. See S11. Authenticate Per Route with a Pre-Request Handler.
|
||||
If you want different auth rules per route rather than a single global check, `set_pre_request_handler()` is a better fit. See [S11. Authenticate per route with a pre-request handler](s11-pre-request).
|
||||
|
||||
> **Note:** If all you want is to modify the response, `set_post_routing_handler()` is the right tool. See S10. Add Response Headers with a Post-Routing Handler.
|
||||
> **Note:** If all you want is to modify the response, `set_post_routing_handler()` is the right tool. See [S10. Add response headers with a post-routing handler](s10-post-routing).
|
||||
|
||||
@@ -4,7 +4,7 @@ order: 30
|
||||
status: "draft"
|
||||
---
|
||||
|
||||
The `set_pre_routing_handler()` from S09 runs **before routing**, so it has no idea which route matched. When you want per-route behavior, `set_pre_request_handler()` is what you need.
|
||||
The `set_pre_routing_handler()` from [S09. Add pre-processing to all routes](s09-pre-routing) runs **before routing**, so it has no idea which route matched. When you want per-route behavior, `set_pre_request_handler()` is what you need.
|
||||
|
||||
## Pre-routing vs. pre-request
|
||||
|
||||
@@ -44,4 +44,4 @@ Same as pre-routing — return `HandlerResponse`.
|
||||
|
||||
## Passing auth info to the route handler
|
||||
|
||||
To pass decoded user info into the route handler, use `res.user_data`. See S12. Pass Data Between Handlers with `res.user_data`.
|
||||
To pass decoded user info into the route handler, use `res.user_data`. See [S12. Pass data between handlers with `res.user_data`](s12-user-data).
|
||||
|
||||
@@ -48,4 +48,4 @@ svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
|
||||
|
||||
Now every error comes back in a consistent JSON shape.
|
||||
|
||||
> **Note:** `set_error_handler()` also fires for 500 responses caused by exceptions thrown from a route handler. To get at the exception itself, combine it with `set_exception_handler()`. See S14. Catch Exceptions.
|
||||
> **Note:** `set_error_handler()` also fires for 500 responses caused by exceptions thrown from a route handler. To get at the exception itself, combine it with `set_exception_handler()`. See [S14. Catch exceptions](s14-exception-handler).
|
||||
|
||||
@@ -59,6 +59,6 @@ svr.set_logger([](const auto &req, const auto &res) {
|
||||
});
|
||||
```
|
||||
|
||||
For more on `user_data`, see S12. Pass Data Between Handlers with `res.user_data`.
|
||||
For more on `user_data`, see [S12. Pass data between handlers with `res.user_data`](s12-user-data).
|
||||
|
||||
> **Note:** The logger runs synchronously on the same thread as request processing. Heavy work inside it hurts throughput — push it to a queue and process asynchronously if you need anything expensive.
|
||||
|
||||
@@ -49,4 +49,4 @@ Because the port is assigned at runtime, parallel test runs don't collide.
|
||||
|
||||
> **Note:** `bind_to_any_port()` returns `-1` on failure (permission errors, no available ports, etc.). Always check the return value.
|
||||
|
||||
> To stop the server, see S19. Shut Down Gracefully.
|
||||
> To stop the server, see [S19. Shut down gracefully](s19-graceful-shutdown).
|
||||
|
||||
@@ -54,4 +54,4 @@ if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||
|
||||
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
|
||||
|
||||
> **Note:** To auto-pick a free port, see S17. Bind to Any Available Port. Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
|
||||
> **Note:** To auto-pick a free port, see [S17. Bind to any available port](s17-bind-any-port). Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
|
||||
|
||||
@@ -52,6 +52,6 @@ Set `set_keep_alive_max_count(1)` and every request gets its own connection. Mos
|
||||
|
||||
## Relationship with the thread pool
|
||||
|
||||
A Keep-Alive connection holds a worker thread for its entire lifetime. If `connections × concurrent requests` exceeds the thread pool size, new requests wait. For thread counts, see S21. Configure the Thread Pool.
|
||||
A Keep-Alive connection holds a worker thread for its entire lifetime. If `connections × concurrent requests` exceeds the thread pool size, new requests wait. For thread counts, see [S21. Configure the thread pool](s21-thread-pool).
|
||||
|
||||
> **Note:** For the client side, see C14. Understand Connection Reuse and Keep-Alive Behavior. Even when the server closes the connection on timeout, the client reconnects automatically.
|
||||
> **Note:** For the client side, see [C14. Understand connection reuse and Keep-Alive behavior](c14-keep-alive). Even when the server closes the connection on timeout, the client reconnects automatically.
|
||||
|
||||
@@ -42,8 +42,8 @@ The usual approach is to treat each backend as a build variant and recompile the
|
||||
|
||||
Certificate verification control, standing up an SSLServer, reading the peer certificate — these all share the same API across backends:
|
||||
|
||||
- T02. Control SSL Certificate Verification
|
||||
- T03. Start an SSL/TLS Server
|
||||
- T05. Access the Peer Certificate on the Server Side
|
||||
- [T02. Control SSL certificate verification](t02-cert-verification)
|
||||
- [T03. Start an SSL/TLS server](t03-ssl-server)
|
||||
- [T05. Access the peer certificate on the server](t05-peer-cert)
|
||||
|
||||
> **Note:** On macOS with an OpenSSL-family backend, cpp-httplib automatically loads root certificates from the system keychain (via `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`, on by default). To disable this, define `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`.
|
||||
|
||||
@@ -48,6 +48,6 @@ The certificate itself is still validated, so this is safer than fully disabling
|
||||
|
||||
On most Linux distributions, root certificates live in a single file like `/etc/ssl/certs/ca-certificates.crt`. cpp-httplib reads the OS default store at startup, so for most servers you don't need to configure anything.
|
||||
|
||||
> The same APIs work on the mbedTLS and wolfSSL backends. For choosing between backends, see T01. Choosing Between OpenSSL, mbedTLS, and wolfSSL.
|
||||
> The same APIs work on the mbedTLS and wolfSSL backends. For choosing between backends, see [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](t01-tls-backends).
|
||||
|
||||
> For details on diagnosing failures, see C18. Handle SSL Errors.
|
||||
> For details on diagnosing failures, see [C18. Handle SSL errors](c18-ssl-errors).
|
||||
|
||||
@@ -34,7 +34,7 @@ httplib::SSLServer svr("cert.pem", "key.pem",
|
||||
nullptr, nullptr, "password");
|
||||
```
|
||||
|
||||
The third and fourth arguments are for client certificate verification (mTLS, see T04). For now, pass `nullptr`.
|
||||
The third and fourth arguments are for client certificate verification (mTLS, see [T04. Configure mTLS](t04-mtls)). For now, pass `nullptr`.
|
||||
|
||||
## Load PEM data from memory
|
||||
|
||||
@@ -73,6 +73,6 @@ openssl req -x509 -newkey rsa:2048 -days 365 -nodes \
|
||||
|
||||
In production, use certificates from Let's Encrypt or your internal CA.
|
||||
|
||||
> **Warning:** Binding an HTTPS server to port 443 requires root. For a safe way to do that, see the privilege-drop pattern in S18. Control Startup Order with `listen_after_bind`.
|
||||
> **Warning:** Binding an HTTPS server to port 443 requires root. For a safe way to do that, see the privilege-drop pattern in [S18. Control startup order with `listen_after_bind`](s18-listen-after-bind).
|
||||
|
||||
> For mutual TLS (client certificates), see T04. Configure mTLS.
|
||||
> For mutual TLS (client certificates), see [T04. Configure mTLS](t04-mtls).
|
||||
|
||||
@@ -59,7 +59,7 @@ Note you're using `SSLClient` directly, not `Client`. If the private key has a p
|
||||
|
||||
## Read client info from a handler
|
||||
|
||||
To see which client connected from inside a handler, use `req.peer_cert()`. Details in T05. Access the Peer Certificate on the Server Side.
|
||||
To see which client connected from inside a handler, use `req.peer_cert()`. Details in [T05. Access the peer certificate on the server](t05-peer-cert).
|
||||
|
||||
## Use cases
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ svr.set_pre_request_handler(
|
||||
});
|
||||
```
|
||||
|
||||
Combined with a pre-request handler, you can keep all authorization logic in one place. See S11. Authenticate Per Route with a Pre-Request Handler.
|
||||
Combined with a pre-request handler, you can keep all authorization logic in one place. See [S11. Authenticate per route with a pre-request handler](s11-pre-request).
|
||||
|
||||
## SNI (Server Name Indication)
|
||||
|
||||
@@ -85,4 +85,4 @@ cpp-httplib handles SNI automatically. If one server hosts multiple domains, SNI
|
||||
|
||||
> **Warning:** `req.peer_cert()` only returns a meaningful value when mTLS is enabled and the client actually presented a certificate. For plain TLS, you get an empty `PeerCert`. Always do the `bool` check before using it.
|
||||
|
||||
> To set up mTLS, see T04. Configure mTLS.
|
||||
> To set up mTLS, see [T04. Configure mTLS](t04-mtls).
|
||||
|
||||
@@ -71,7 +71,7 @@ ws.send("Hello"); // text frame
|
||||
ws.send(binary_data, binary_data_size); // binary frame
|
||||
```
|
||||
|
||||
The `std::string` overload sends as **text**; the `const char*` + size overload sends as **binary**. A bit subtle, but once you know it, it's intuitive. See W04. Send and Receive Binary Frames for details.
|
||||
The `std::string` overload sends as **text**; the `const char*` + size overload sends as **binary**. A bit subtle, but once you know it, it's intuitive. See [W04. Send and receive binary frames](w04-websocket-binary) for details.
|
||||
|
||||
## Thread pool implications
|
||||
|
||||
@@ -83,6 +83,6 @@ svr.new_task_queue = [] {
|
||||
};
|
||||
```
|
||||
|
||||
See S21. Configure the Thread Pool.
|
||||
See [S21. Configure the thread pool](s21-thread-pool).
|
||||
|
||||
> **Note:** To run WebSocket over HTTPS, use `httplib::SSLServer` instead of `httplib::Server` — the same `WebSocket()` handler just works. On the client side, use a `wss://` URL.
|
||||
|
||||
@@ -57,4 +57,24 @@ Too short wastes bandwidth; too long and connections get dropped. As a rule of t
|
||||
|
||||
> **Warning:** A very short ping interval spawns background work per connection and increases CPU usage. For servers with many connections, keep the interval modest.
|
||||
|
||||
> For handling a closed connection, see W03. Handle Connection Close.
|
||||
## Detecting an unresponsive peer
|
||||
|
||||
Sending pings alone doesn't tell you anything if the peer just silently dies — the TCP socket might still look open while the process on the other end is long gone. To catch that, enable the max-missed-pongs check: if N consecutive pings go unanswered, the connection is closed.
|
||||
|
||||
```cpp
|
||||
cli.set_websocket_max_missed_pongs(2); // close after 2 consecutive unacked pings
|
||||
```
|
||||
|
||||
The server side has the same `set_websocket_max_missed_pongs()`.
|
||||
|
||||
With a 30-second ping interval and `max_missed_pongs = 2`, a dead peer is detected within roughly 60 seconds and the connection is closed with `CloseStatus::GoingAway` and the reason `"pong timeout"`.
|
||||
|
||||
The counter is reset whenever `read()` consumes an incoming Pong frame, so this only works if your code is actively calling `read()` in a loop — which is what a normal WebSocket client does anyway.
|
||||
|
||||
### Why the default is 0
|
||||
|
||||
`max_missed_pongs` defaults to `0`, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses aren't checked. If you want unresponsive-peer detection, set it explicitly to `1` or higher.
|
||||
|
||||
Even with `0`, a dead connection won't linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. Think of `max_missed_pongs` as the knob for detecting an unresponsive peer **faster** than that.
|
||||
|
||||
> For handling a closed connection, see [W03. Handle connection close](w03-websocket-close).
|
||||
|
||||
@@ -56,7 +56,7 @@ Binary frames still come back in a `std::string`, but treat its contents as raw
|
||||
|
||||
## Ping is binary-ish, but hidden
|
||||
|
||||
WebSocket Ping/Pong frames are close cousins of binary frames at the opcode level, but cpp-httplib handles them automatically — you don't touch them. See W02. Set a WebSocket Heartbeat.
|
||||
WebSocket Ping/Pong frames are close cousins of binary frames at the opcode level, but cpp-httplib handles them automatically — you don't touch them. See [W02. Set a WebSocket heartbeat](w02-websocket-ping).
|
||||
|
||||
## Example: send an image
|
||||
|
||||
|
||||
@@ -57,4 +57,4 @@ auto res = cli.Get("/large-file",
|
||||
>
|
||||
> `ResponseHandler`はヘッダー受信後、ボディ受信前に呼ばれます。`false`を返せばダウンロード自体をスキップできます。
|
||||
|
||||
> ダウンロードの進捗を表示したい場合はC11. 進捗コールバックを使うを参照してください。
|
||||
> ダウンロードの進捗を表示したい場合は[C11. 進捗コールバックを使う](c11-progress-callback)を参照してください。
|
||||
|
||||
@@ -31,6 +31,6 @@ if (res && res->status == 200) {
|
||||
|
||||
`res->body`は`std::string`なので、そのままJSONライブラリに渡せます。
|
||||
|
||||
> **Note:** サーバーがエラー時にHTMLを返すことがあります。ステータスコードを確認してからパースすると安全です。また、APIによっては`Accept: application/json`ヘッダーが必要です。JSON APIを繰り返し呼ぶならC03. デフォルトヘッダーを設定するが便利です。
|
||||
> **Note:** サーバーがエラー時にHTMLを返すことがあります。ステータスコードを確認してからパースすると安全です。また、APIによっては`Accept: application/json`ヘッダーが必要です。JSON APIを繰り返し呼ぶなら[C03. デフォルトヘッダーを設定する](c03-default-headers)が便利です。
|
||||
|
||||
> サーバー側でJSONを受け取って返す方法はS02. JSONリクエストを受け取りJSONレスポンスを返すを参照してください。
|
||||
> サーバー側でJSONを受け取って返す方法は[S02. JSONリクエストを受け取りJSONレスポンスを返す](s02-json-api)を参照してください。
|
||||
|
||||
@@ -52,4 +52,4 @@ auto res = cli.Get("/users", headers);
|
||||
|
||||
リクエスト単位で渡したヘッダーはデフォルトヘッダーに**追加**されます。両方がサーバーに送られます。
|
||||
|
||||
> Bearerトークンを使った認証の詳細はC06. BearerトークンでAPIを呼ぶを参照してください。
|
||||
> Bearerトークンを使った認証の詳細は[C06. BearerトークンでAPIを呼ぶ](c06-bearer-token)を参照してください。
|
||||
|
||||
@@ -35,4 +35,4 @@ auto res = cli.Get("/");
|
||||
|
||||
> **Warning:** HTTPSへのリダイレクトを追従するには、cpp-httplibをOpenSSL(または他のTLSバックエンド)付きでビルドしておく必要があります。TLSサポートがないと、HTTPSへのリダイレクトは失敗します。
|
||||
|
||||
> **Note:** リダイレクトを追従すると、リクエストの実行時間は伸びます。タイムアウトの設定はC12. タイムアウトを設定するを参照してください。
|
||||
> **Note:** リダイレクトを追従すると、リクエストの実行時間は伸びます。タイムアウトの設定は[C12. タイムアウトを設定する](c12-timeouts)を参照してください。
|
||||
|
||||
@@ -43,4 +43,4 @@ auto res = cli.Get("/private", headers);
|
||||
cli.set_digest_auth("alice", "s3cret");
|
||||
```
|
||||
|
||||
> BearerトークンでAPIを呼びたい場合はC06. BearerトークンでAPIを呼ぶを参照してください。
|
||||
> BearerトークンでAPIを呼びたい場合は[C06. BearerトークンでAPIを呼ぶ](c06-bearer-token)を参照してください。
|
||||
|
||||
@@ -47,4 +47,4 @@ if (res && res->status == 401) {
|
||||
|
||||
> **Warning:** Bearerトークンはそれ自体が認証情報です。必ずHTTPS経由で送ってください。また、ソースコードや設定ファイルにトークンをハードコードしないようにしましょう。
|
||||
|
||||
> 複数のヘッダーをまとめて設定したいときはC03. デフォルトヘッダーを設定するも便利です。
|
||||
> 複数のヘッダーをまとめて設定したいときは[C03. デフォルトヘッダーを設定する](c03-default-headers)も便利です。
|
||||
|
||||
@@ -49,4 +49,4 @@ auto res = cli.Post("/upload", httplib::Headers{}, items, provider_items);
|
||||
|
||||
> **Note:** `UploadFormDataItems`と`FormDataProviderItems`は同じリクエスト内で併用できます。テキストフィールドは`UploadFormDataItems`、ファイルは`FormDataProviderItems`、という使い分けがきれいです。
|
||||
|
||||
> アップロードの進捗を表示したい場合はC11. 進捗コールバックを使うを参照してください。
|
||||
> アップロードの進捗を表示したい場合は[C11. 進捗コールバックを使う](c11-progress-callback)を参照してください。
|
||||
|
||||
@@ -31,4 +31,4 @@ auto res = cli.Put("/bucket/backup.tar.gz", size,
|
||||
|
||||
> **Warning:** `make_file_body()`はContent-Lengthを最初に確定させる必要があるため、ファイルサイズをあらかじめ取得します。送信中にファイルサイズが変わる可能性がある場合は、このAPIには向きません。
|
||||
|
||||
> マルチパート形式で送りたい場合はC07. ファイルをマルチパートフォームとしてアップロードするを参照してください。
|
||||
> マルチパート形式で送りたい場合は[C07. ファイルをマルチパートフォームとしてアップロードする](c07-multipart-upload)を参照してください。
|
||||
|
||||
@@ -44,4 +44,4 @@ auto res = cli.Post("/upload", total_size,
|
||||
|
||||
> **Detail:** `sink.write()`は書き込みが成功したかどうかを`bool`で返します。`false`が返ったら回線が切れています。ラムダはそのまま`false`を返して終了しましょう。
|
||||
|
||||
> ファイルをそのまま送るだけなら、`make_file_body()`が便利です。C08. ファイルを生バイナリとしてPOSTするを参照してください。
|
||||
> ファイルをそのまま送るだけなら、`make_file_body()`が便利です。[C08. ファイルを生バイナリとしてPOSTする](c08-post-file-body)を参照してください。
|
||||
|
||||
@@ -48,5 +48,5 @@ auto res = cli.Get("/events",
|
||||
|
||||
> **Warning:** `ContentReceiver`を渡すと、`res->body`は**空のまま**になります。ボディは自分でコールバック内で保存するか処理するかしてください。
|
||||
|
||||
> ダウンロードの進捗を知りたい場合はC11. 進捗コールバックを使うと組み合わせましょう。
|
||||
> Server-Sent Events(SSE)を扱うときはE04. SSEをクライアントで受信するも参考になります。
|
||||
> ダウンロードの進捗を知りたい場合は[C11. 進捗コールバックを使う](c11-progress-callback)と組み合わせましょう。
|
||||
> Server-Sent Events(SSE)を扱うときは[E04. SSEをクライアントで受信する](e04-sse-client)も参考になります。
|
||||
|
||||
@@ -56,4 +56,4 @@ auto res = cli.Get("/large-file",
|
||||
|
||||
> **Note:** `ContentReceiver`と進捗コールバックは同時に使えます。ファイルに書き出しながら進捗を表示したいときは、両方を渡しましょう。
|
||||
|
||||
> ファイル保存と組み合わせる具体例はC01. レスポンスボディを取得する / ファイルに保存するも参照してください。
|
||||
> ファイル保存と組み合わせる具体例は[C01. レスポンスボディを取得する / ファイルに保存する](c01-get-response-body)も参照してください。
|
||||
|
||||
@@ -47,4 +47,4 @@ cli.set_connection_timeout(3s);
|
||||
cli.set_read_timeout(10s);
|
||||
```
|
||||
|
||||
> **Warning:** 読み取りタイムアウトは「1回の受信待ち」に対するタイムアウトです。大きなファイルのダウンロードで途中ずっとデータが流れている限り、リクエスト全体で30分かかっても発火しません。リクエスト全体の時間制限を設けたい場合はC13. 全体タイムアウトを設定するを使ってください。
|
||||
> **Warning:** 読み取りタイムアウトは「1回の受信待ち」に対するタイムアウトです。大きなファイルのダウンロードで途中ずっとデータが流れている限り、リクエスト全体で30分かかっても発火しません。リクエスト全体の時間制限を設けたい場合は[C13. 全体タイムアウトを設定する](c13-max-timeout)を使ってください。
|
||||
|
||||
@@ -4,7 +4,7 @@ order: 13
|
||||
status: "draft"
|
||||
---
|
||||
|
||||
C12. タイムアウトを設定するで紹介した3種類のタイムアウトは、いずれも「1回の`send`や`recv`」に対するものです。リクエスト全体の所要時間に上限を設けたい場合は、`set_max_timeout()`を使います。
|
||||
[C12. タイムアウトを設定する](c12-timeouts)で紹介した3種類のタイムアウトは、いずれも「1回の`send`や`recv`」に対するものです。リクエスト全体の所要時間に上限を設けたい場合は、`set_max_timeout()`を使います。
|
||||
|
||||
## 基本の使い方
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ cli.set_proxy_digest_auth("user", "password");
|
||||
|
||||
## エンドのサーバー認証と組み合わせる
|
||||
|
||||
プロキシ認証と、エンドサーバーへの認証(C05やC06)は別物です。両方が必要なら、両方設定します。
|
||||
プロキシ認証と、エンドサーバーへの認証([C05. Basic認証を使う](c05-basic-auth)や[C06. BearerトークンでAPIを呼ぶ](c06-bearer-token))は別物です。両方が必要なら、両方設定します。
|
||||
|
||||
```cpp
|
||||
cli.set_proxy("proxy.internal", 8080);
|
||||
|
||||
@@ -60,4 +60,4 @@ std::cout << res->body << std::endl;
|
||||
|
||||
ネットワーク層のエラーは`res.error()`、HTTPのエラーは`res->status`、と頭の中で分けておきましょう。
|
||||
|
||||
> SSL関連のエラーをさらに詳しく調べたい場合はC18. SSLエラーをハンドリングするを参照してください。
|
||||
> SSL関連のエラーをさらに詳しく調べたい場合は[C18. SSLエラーをハンドリングする](c18-ssl-errors)を参照してください。
|
||||
|
||||
@@ -48,6 +48,4 @@ if (res.ssl_backend_error() != 0) {
|
||||
| `SSLServerHostnameVerification` | 証明書のCN/SANとホスト名が一致しない |
|
||||
| `SSLConnection` | TLSバージョンの不一致、対応スイートが無い |
|
||||
|
||||
> **Note:** `ssl_backend_error()`は以前は`ssl_openssl_error()`と呼ばれていました。後者はdeprecatedで、現在は`ssl_backend_error()`を使ってください。
|
||||
|
||||
> 証明書の検証設定を変えたい場合はT02. SSL証明書の検証を制御するを参照してください。
|
||||
> 証明書の検証設定を変えたい場合は[T02. SSL証明書の検証を制御する](t02-cert-verification)を参照してください。
|
||||
|
||||
@@ -56,7 +56,7 @@ svr.Get("/time", [](const httplib::Request &req, httplib::Response &res) {
|
||||
});
|
||||
```
|
||||
|
||||
クライアントが切断したら`sink.done()`で終了します。詳しくはS16. クライアントが切断したか検出するを参照してください。
|
||||
クライアントが切断したら`sink.done()`で終了します。詳しくは[S16. クライアントが切断したか検出する](s16-disconnect)を参照してください。
|
||||
|
||||
## コメント行でハートビート
|
||||
|
||||
@@ -80,8 +80,8 @@ svr.new_task_queue = [] {
|
||||
};
|
||||
```
|
||||
|
||||
詳しくはS21. マルチスレッド数を設定するを参照してください。
|
||||
詳しくは[S21. マルチスレッド数を設定する](s21-thread-pool)を参照してください。
|
||||
|
||||
> **Note:** `data:`の後ろに改行が含まれる場合、各行の先頭に`data: `を付けて複数の`data:`行として送ります。SSEの仕様で決まっているフォーマットです。
|
||||
|
||||
> イベント名を使い分けたい場合はE02. SSEでイベント名を使い分けるを、クライアント側はE04. SSEをクライアントで受信するを参照してください。
|
||||
> イベント名を使い分けたい場合は[E02. SSEでイベント名を使い分ける](e02-sse-event-names)を、クライアント側は[E04. SSEをクライアントで受信する](e04-sse-client)を参照してください。
|
||||
|
||||
@@ -52,7 +52,7 @@ auto send_event = [](httplib::DataSink &sink,
|
||||
send_event(sink, "message", "Hello!", "42");
|
||||
```
|
||||
|
||||
IDの付け方は自由です。連番でもUUIDでも、サーバー側で重複せず順序が追えるものを選びましょう。再接続の詳細はE03. SSEの再接続を処理するを参照してください。
|
||||
IDの付け方は自由です。連番でもUUIDでも、サーバー側で重複せず順序が追えるものを選びましょう。再接続の詳細は[E03. SSEの再接続を処理する](e03-sse-reconnect)を参照してください。
|
||||
|
||||
## JSONをdataに乗せる
|
||||
|
||||
|
||||
@@ -96,4 +96,4 @@ std::cout << "last id: " << sse.last_event_id() << std::endl;
|
||||
|
||||
> **Note:** SSEClientの`start()`はブロッキングなので、単発のツールならそのまま使えますが、GUIアプリやサーバーに組み込むときは`start_async()` + `stop()`の組み合わせが基本です。
|
||||
|
||||
> サーバー側の実装はE01. SSEサーバーを実装するを参照してください。
|
||||
> サーバー側の実装は[E01. SSEサーバーを実装する](e01-sse-server)を参照してください。
|
||||
|
||||
@@ -61,6 +61,6 @@ svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
|
||||
|
||||
レスポンスヘッダーを追加したいときは`res.set_header("Name", "Value")`です。
|
||||
|
||||
> **Note:** `listen()`はブロックする関数です。別スレッドで動かしたいときは`std::thread`で包むか、ノンブロッキング起動が必要ならS18. `listen_after_bind`で起動順序を制御するを参照してください。
|
||||
> **Note:** `listen()`はブロックする関数です。別スレッドで動かしたいときは`std::thread`で包むか、ノンブロッキング起動が必要なら[S18. `listen_after_bind`で起動順序を制御する](s18-listen-after-bind)を参照してください。
|
||||
|
||||
> パスパラメーター(`/users/:id`)を使いたい場合はS03. パスパラメーターを使うを参照してください。
|
||||
> パスパラメーター(`/users/:id`)を使いたい場合は[S03. パスパラメーターを使う](s03-path-params)を参照してください。
|
||||
|
||||
@@ -69,6 +69,6 @@ svr.Get("/api/health", [&](const auto &req, auto &res) {
|
||||
});
|
||||
```
|
||||
|
||||
> **Note:** 大きなJSONボディを受け取ると、`req.body`がまるごとメモリに載ります。巨大なペイロードを扱うときはS07. マルチパートデータをストリーミングで受け取るのように、ストリーミング受信も検討しましょう。
|
||||
> **Note:** 大きなJSONボディを受け取ると、`req.body`がまるごとメモリに載ります。巨大なペイロードを扱うときは[S07. マルチパートデータをストリーミングで受け取る](s07-multipart-reader)のように、ストリーミング受信も検討しましょう。
|
||||
|
||||
> クライアント側の書き方はC02. JSONを送受信するを参照してください。
|
||||
> クライアント側の書き方は[C02. JSONを送受信する](c02-json)を参照してください。
|
||||
|
||||
@@ -52,4 +52,4 @@ svr.set_file_extension_and_mimetype_mapping("wasm", "application/wasm");
|
||||
|
||||
> **Warning:** 静的ファイル配信系のメソッドは**スレッドセーフではありません**。起動後(`listen()`以降)には呼ばないでください。起動前にまとめて設定しましょう。
|
||||
|
||||
> ダウンロード用のレスポンスを返したい場合はS06. ファイルダウンロードレスポンスを返すも参考になります。
|
||||
> ダウンロード用のレスポンスを返したい場合は[S06. ファイルダウンロードレスポンスを返す](s06-download-response)も参考になります。
|
||||
|
||||
@@ -60,4 +60,4 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
|
||||
|
||||
> **Note:** プロバイダラムダは複数回呼ばれます。キャプチャする変数のライフタイムに気をつけてください。必要なら`std::shared_ptr`などで包みましょう。
|
||||
|
||||
> ファイルダウンロードとして扱いたい場合はS06. ファイルダウンロードレスポンスを返すを参照してください。
|
||||
> ファイルダウンロードとして扱いたい場合は[S06. ファイルダウンロードレスポンスを返す](s06-download-response)を参照してください。
|
||||
|
||||
@@ -68,4 +68,4 @@ svr.Post("/upload",
|
||||
|
||||
> **Warning:** `HandlerWithContentReader`を使うと、`req.body`は**空のまま**です。ボディはコールバック内で自分で処理してください。
|
||||
|
||||
> クライアント側でマルチパートを送る方法はC07. ファイルをマルチパートフォームとしてアップロードするを参照してください。
|
||||
> クライアント側でマルチパートを送る方法は[C07. ファイルをマルチパートフォームとしてアップロードする](c07-multipart-upload)を参照してください。
|
||||
|
||||
@@ -50,4 +50,4 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
|
||||
|
||||
> **Note:** 小さなレスポンスは圧縮しても効果が薄く、むしろCPU時間を無駄にすることがあります。cpp-httplibは小さすぎるボディは圧縮をスキップします。
|
||||
|
||||
> クライアント側の挙動はC15. 圧縮を有効にするを参照してください。
|
||||
> クライアント側の挙動は[C15. 圧縮を有効にする](c15-compression)を参照してください。
|
||||
|
||||
@@ -49,6 +49,6 @@ svr.set_pre_routing_handler(
|
||||
|
||||
## 特定ルートだけに認証をかけたい場合
|
||||
|
||||
全ルート共通ではなく、ルート単位で認証を分けたいときは、S11. Pre-request handlerでルート単位の認証を行うのほうが適しています。
|
||||
全ルート共通ではなく、ルート単位で認証を分けたいときは、[S11. Pre-request handlerでルート単位の認証を行う](s11-pre-request)のほうが適しています。
|
||||
|
||||
> **Note:** レスポンスを加工したいだけなら、`set_post_routing_handler()`のほうが適切です。S10. Post-routing handlerでレスポンスヘッダーを追加するを参照してください。
|
||||
> **Note:** レスポンスを加工したいだけなら、`set_post_routing_handler()`のほうが適切です。[S10. Post-routing handlerでレスポンスヘッダーを追加する](s10-post-routing)を参照してください。
|
||||
|
||||
@@ -4,7 +4,7 @@ order: 30
|
||||
status: "draft"
|
||||
---
|
||||
|
||||
S09で紹介した`set_pre_routing_handler()`はルーティングの**前**に呼ばれるので、「どのルートにマッチしたか」を知れません。ルートによって認証の有無を変えたい場合は、`set_pre_request_handler()`のほうが便利です。
|
||||
[S09. 全ルートに共通の前処理をする](s09-pre-routing)で紹介した`set_pre_routing_handler()`はルーティングの**前**に呼ばれるので、「どのルートにマッチしたか」を知れません。ルートによって認証の有無を変えたい場合は、`set_pre_request_handler()`のほうが便利です。
|
||||
|
||||
## Pre-routingとの違い
|
||||
|
||||
@@ -44,4 +44,4 @@ Pre-routingハンドラと同じく、`HandlerResponse`を返します。
|
||||
|
||||
## 認証情報を後続のハンドラに渡す
|
||||
|
||||
認証で取り出したユーザー情報などをルートハンドラに渡したいときは、`res.user_data`を使います。詳しくはS12. `res.user_data`でハンドラ間データを渡すを参照してください。
|
||||
認証で取り出したユーザー情報などをルートハンドラに渡したいときは、`res.user_data`を使います。詳しくは[S12. `res.user_data`でハンドラ間データを渡す](s12-user-data)を参照してください。
|
||||
|
||||
@@ -48,4 +48,4 @@ svr.set_error_handler([](const httplib::Request &req, httplib::Response &res) {
|
||||
|
||||
これで全エラーが統一されたJSONで返ります。
|
||||
|
||||
> **Note:** `set_error_handler()`は、ルートハンドラが例外を投げた場合の500エラーにも呼ばれます。例外そのものの情報を取り出したい場合は`set_exception_handler()`を組み合わせましょう。S14. 例外をキャッチするを参照してください。
|
||||
> **Note:** `set_error_handler()`は、ルートハンドラが例外を投げた場合の500エラーにも呼ばれます。例外そのものの情報を取り出したい場合は`set_exception_handler()`を組み合わせましょう。[S14. 例外をキャッチする](s14-exception-handler)を参照してください。
|
||||
|
||||
@@ -59,6 +59,6 @@ svr.set_logger([](const auto &req, const auto &res) {
|
||||
});
|
||||
```
|
||||
|
||||
`user_data`の使い方はS12. `res.user_data`でハンドラ間データを渡すも参照してください。
|
||||
`user_data`の使い方は[S12. `res.user_data`でハンドラ間データを渡す](s12-user-data)も参照してください。
|
||||
|
||||
> **Note:** ロガーはリクエスト処理と同じスレッドで同期的に呼ばれます。重い処理を直接入れると全体のスループットが落ちるので、必要ならキューに流して非同期で処理しましょう。
|
||||
|
||||
@@ -49,4 +49,4 @@ t.join();
|
||||
|
||||
> **Note:** `bind_to_any_port()`は失敗すると`-1`を返します。権限エラーや利用可能ポートが無いケースなので、返り値のチェックを忘れずに。
|
||||
|
||||
> サーバーを止める方法はS19. グレースフルシャットダウンするを参照してください。
|
||||
> サーバーを止める方法は[S19. グレースフルシャットダウンする](s19-graceful-shutdown)を参照してください。
|
||||
|
||||
@@ -54,4 +54,4 @@ if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||
|
||||
`listen_after_bind()`はサーバーが停止するまでブロックし、正常終了なら`true`を返します。
|
||||
|
||||
> **Note:** 空いているポートを自動で選びたいときはS17. ポートを動的に割り当てるを参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
|
||||
> **Note:** 空いているポートを自動で選びたいときは[S17. ポートを動的に割り当てる](s17-bind-any-port)を参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
|
||||
|
||||
@@ -52,6 +52,6 @@ svr.set_keep_alive_max_count(1000);
|
||||
|
||||
## スレッドプールとの関係
|
||||
|
||||
Keep-Aliveでつながりっぱなしの接続は、その間ずっとワーカースレッドを1つ占有します。接続数 × 同時リクエスト数がスレッドプールのサイズを超えると、新しいリクエストが待たされます。スレッド数の調整はS21. マルチスレッド数を設定するを参照してください。
|
||||
Keep-Aliveでつながりっぱなしの接続は、その間ずっとワーカースレッドを1つ占有します。接続数 × 同時リクエスト数がスレッドプールのサイズを超えると、新しいリクエストが待たされます。スレッド数の調整は[S21. マルチスレッド数を設定する](s21-thread-pool)を参照してください。
|
||||
|
||||
> **Note:** クライアント側の挙動はC14. 接続の再利用とKeep-Aliveの挙動を理解するを参照してください。サーバーがタイムアウトで接続を切っても、クライアントは自動で再接続します。
|
||||
> **Note:** クライアント側の挙動は[C14. 接続の再利用とKeep-Aliveの挙動を理解する](c14-keep-alive)を参照してください。サーバーがタイムアウトで接続を切っても、クライアントは自動で再接続します。
|
||||
|
||||
@@ -42,8 +42,8 @@ wolfSSLには商用ライセンスとサポートがあります。製品に組
|
||||
|
||||
証明書の検証制御、SSLServerの立ち上げ、ピア証明書の取得などは、どのバックエンドでも同じAPIで呼べます。
|
||||
|
||||
- T02. SSL証明書の検証を制御する
|
||||
- T03. SSL/TLSサーバーを立ち上げる
|
||||
- T05. サーバー側でピア証明書を参照する
|
||||
- [T02. SSL証明書の検証を制御する](t02-cert-verification)
|
||||
- [T03. SSL/TLSサーバーを立ち上げる](t03-ssl-server)
|
||||
- [T05. サーバー側でピア証明書を参照する](t05-peer-cert)
|
||||
|
||||
> **Note:** macOSでは、OpenSSL系のバックエンドを使う場合、システムのキーチェーンからルート証明書を自動で読む設定(`CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`)がデフォルトで有効です。無効にしたい場合は`CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES`を定義してください。
|
||||
|
||||
@@ -48,6 +48,6 @@ cli.enable_server_hostname_verification(false);
|
||||
|
||||
多くのLinuxディストリビューションでは、`/etc/ssl/certs/ca-certificates.crt`などにルート証明書がまとまっています。cpp-httplibは起動時にOSのデフォルトストアを自動で読みにいくので、普通のサーバーならとくに設定不要です。
|
||||
|
||||
> mbedTLSやwolfSSLバックエンドでも同じAPIが使えます。バックエンドの選び方はT01. OpenSSL・mbedTLS・wolfSSLの選択指針を参照してください。
|
||||
> mbedTLSやwolfSSLバックエンドでも同じAPIが使えます。バックエンドの選び方は[T01. OpenSSL・mbedTLS・wolfSSLの選択指針](t01-tls-backends)を参照してください。
|
||||
|
||||
> 失敗したときの詳細を調べる方法はC18. SSLエラーをハンドリングするを参照してください。
|
||||
> 失敗したときの詳細を調べる方法は[C18. SSLエラーをハンドリングする](c18-ssl-errors)を参照してください。
|
||||
|
||||
@@ -34,7 +34,7 @@ httplib::SSLServer svr("cert.pem", "key.pem",
|
||||
nullptr, nullptr, "password");
|
||||
```
|
||||
|
||||
第3、第4引数はクライアント証明書検証用(mTLS、T04参照)なので、今は`nullptr`を指定します。
|
||||
第3、第4引数はクライアント証明書検証用(mTLS、[T04. mTLSを設定する](t04-mtls)参照)なので、今は`nullptr`を指定します。
|
||||
|
||||
## メモリ上のPEMから立ち上げる
|
||||
|
||||
@@ -73,6 +73,6 @@ openssl req -x509 -newkey rsa:2048 -days 365 -nodes \
|
||||
|
||||
本番では、Let's Encryptや社内CAから発行された証明書を使いましょう。
|
||||
|
||||
> **Warning:** HTTPSサーバーを443番ポートで立ち上げるにはroot権限が必要です。安全に立ち上げる方法はS18. `listen_after_bind`で起動順序を制御するの「特権降格」を参照してください。
|
||||
> **Warning:** HTTPSサーバーを443番ポートで立ち上げるにはroot権限が必要です。安全に立ち上げる方法は[S18. `listen_after_bind`で起動順序を制御する](s18-listen-after-bind)の「特権降格」を参照してください。
|
||||
|
||||
> クライアント証明書による相互認証(mTLS)はT04. mTLSを設定するを参照してください。
|
||||
> クライアント証明書による相互認証(mTLS)は[T04. mTLSを設定する](t04-mtls)を参照してください。
|
||||
|
||||
@@ -59,7 +59,7 @@ auto res = cli.Get("/");
|
||||
|
||||
## ハンドラからクライアント情報を取得する
|
||||
|
||||
ハンドラの中で、どのクライアントが接続してきたかを確認したいときは`req.peer_cert()`を使います。詳しくはT05. サーバー側でピア証明書を参照するを参照してください。
|
||||
ハンドラの中で、どのクライアントが接続してきたかを確認したいときは`req.peer_cert()`を使います。詳しくは[T05. サーバー側でピア証明書を参照する](t05-peer-cert)を参照してください。
|
||||
|
||||
## 用途
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ svr.set_pre_request_handler(
|
||||
});
|
||||
```
|
||||
|
||||
Pre-requestハンドラと組み合わせれば、共通の認可ロジックを一箇所にまとめられます。詳しくはS11. Pre-request handlerでルート単位の認証を行うを参照してください。
|
||||
Pre-requestハンドラと組み合わせれば、共通の認可ロジックを一箇所にまとめられます。詳しくは[S11. Pre-request handlerでルート単位の認証を行う](s11-pre-request)を参照してください。
|
||||
|
||||
## SNI(Server Name Indication)
|
||||
|
||||
@@ -85,4 +85,4 @@ Pre-requestハンドラと組み合わせれば、共通の認可ロジックを
|
||||
|
||||
> **Warning:** `req.peer_cert()`は、mTLSが有効で、かつクライアントが証明書を提示した場合のみ有効な値を返します。通常のTLS接続では空の`PeerCert`が返ります。使う前に必ず`bool`チェックしてください。
|
||||
|
||||
> mTLSの設定方法はT04. mTLSを設定するを参照してください。
|
||||
> mTLSの設定方法は[T04. mTLSを設定する](t04-mtls)を参照してください。
|
||||
|
||||
@@ -71,7 +71,7 @@ ws.send("Hello"); // テキストフレーム
|
||||
ws.send(binary_data, binary_data_size); // バイナリフレーム
|
||||
```
|
||||
|
||||
`std::string`を受け取るオーバーロードはテキスト、`const char*`とサイズを受け取るオーバーロードはバイナリとして送られます。詳しくはW04. バイナリフレームを送受信するを参照してください。
|
||||
`std::string`を受け取るオーバーロードはテキスト、`const char*`とサイズを受け取るオーバーロードはバイナリとして送られます。詳しくは[W04. バイナリフレームを送受信する](w04-websocket-binary)を参照してください。
|
||||
|
||||
## スレッドとの関係
|
||||
|
||||
@@ -83,6 +83,6 @@ svr.new_task_queue = [] {
|
||||
};
|
||||
```
|
||||
|
||||
詳細はS21. マルチスレッド数を設定するを参照してください。
|
||||
詳細は[S21. マルチスレッド数を設定する](s21-thread-pool)を参照してください。
|
||||
|
||||
> **Note:** HTTPSサーバーの上でWebSocketを動かしたいときは、`httplib::Server`の代わりに`httplib::SSLServer`を使えば、同じ`WebSocket()`ハンドラがそのまま動きます。クライアント側は`wss://`スキームを指定するだけです。
|
||||
|
||||
@@ -57,4 +57,24 @@ WebSocketプロトコルでは、PingフレームにはPongフレームで応答
|
||||
|
||||
> **Warning:** Ping間隔を極端に短くすると、WebSocket接続ごとにバックグラウンドでスレッドが走るので、CPU負荷が上がります。接続数が多いサーバーでは控えめな値に設定しましょう。
|
||||
|
||||
> 接続が閉じたときの処理はW03. 接続クローズをハンドリングするを参照してください。
|
||||
## 無応答のピアを検出する
|
||||
|
||||
Pingを送るだけでは、相手が「黙って落ちた」場合に気付けません。TCPの接続自体は生きているように見えるのに、相手のプロセスはもう応答しない、というケースです。これを検出するには、送ったPingに対してPongがN回連続で返ってこなかったら接続を切る、というオプションを有効にします。
|
||||
|
||||
```cpp
|
||||
cli.set_websocket_max_missed_pongs(2); // 2回連続でPongが返ってこなければ切断
|
||||
```
|
||||
|
||||
サーバー側にも同じ`set_websocket_max_missed_pongs()`があります。
|
||||
|
||||
たとえばPing間隔が30秒で`max_missed_pongs = 2`なら、無応答のピアは約60秒で検出され、`CloseStatus::GoingAway`(理由は`"pong timeout"`)で接続が閉じられます。
|
||||
|
||||
この仕組みは`read()`を呼んでPongフレームを消費したタイミングでカウンタがリセットされます。つまり通常のWebSocketクライアントのように`read()`をループで回していれば、特に意識することなく動きます。
|
||||
|
||||
### デフォルトは無効
|
||||
|
||||
`max_missed_pongs`のデフォルトは`0`で、これは「Pongが何回返ってこなくてもこの仕組みでは切断しない」という意味です。Ping自体は送られ続けますが、応答の有無はチェックされません。無応答ピアを検出したい場合は明示的に`1`以上を設定してください。
|
||||
|
||||
ただし`0`のままでも最終的に接続が残り続けることはありません。`read()`を呼んでいる間は`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`(デフォルト**300秒 = 5分**)が保険として働き、フレームが一定時間来なければ`read()`が失敗します。つまり`max_missed_pongs`は「**もっと速く**無応答を検出したい」ときに使うオプションだと考えてください。
|
||||
|
||||
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](w03-websocket-close)を参照してください。
|
||||
|
||||
@@ -56,7 +56,7 @@ switch (result) {
|
||||
|
||||
## Pingもバイナリフレームの一種
|
||||
|
||||
WebSocketのPing/PongフレームもOpcodeレベルではバイナリに近い扱いですが、cpp-httplibが自動で処理するので、アプリケーションコードで意識する必要はありません。W02. ハートビートを設定するを参照してください。
|
||||
WebSocketのPing/PongフレームもOpcodeレベルではバイナリに近い扱いですが、cpp-httplibが自動で処理するので、アプリケーションコードで意識する必要はありません。[W02. ハートビートを設定する](w02-websocket-ping)を参照してください。
|
||||
|
||||
## サンプル: 画像を送る
|
||||
|
||||
|
||||
+1
-4
@@ -30,10 +30,7 @@ int main(void) {
|
||||
} else {
|
||||
cout << "error code: " << res.error() << std::endl;
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
auto result = cli.get_verify_result();
|
||||
if (result) {
|
||||
cout << "verify error: " << X509_verify_cert_error_string(result) << endl;
|
||||
}
|
||||
cout << "ssl backend error: " << res.ssl_backend_error() << endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+47
-16
@@ -18,6 +18,8 @@ ifneq ($(OS), Windows_NT)
|
||||
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -lssl -lcrypto
|
||||
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -lmbedtls -lmbedx509 -lmbedcrypto
|
||||
WOLFSSL_SUPPORT = -DCPPHTTPLIB_WOLFSSL_SUPPORT -lwolfssl
|
||||
# Disable ASLR for ASAN compatibility on WSL2 (high-entropy ASLR conflicts with ASAN shadow memory)
|
||||
SETARCH = setarch $(shell uname -m) -R
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -59,28 +61,35 @@ STYLE_CHECK_FILES = $(filter-out httplib.h httplib.cc, \
|
||||
$(wildcard example/*.h example/*.cc fuzzing/*.h fuzzing/*.cc *.h *.cc ../httplib.h))
|
||||
|
||||
all : test test_split
|
||||
LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test
|
||||
LSAN_OPTIONS=suppressions=lsan_suppressions.txt $(SETARCH) ./test
|
||||
|
||||
SHARDS ?= 4
|
||||
|
||||
define run_parallel
|
||||
@echo "Running $(1) with $(SHARDS) shards in parallel..."
|
||||
@fail=0; \
|
||||
@fail=0; pids=""; \
|
||||
for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
|
||||
GTEST_TOTAL_SHARDS=$(SHARDS) GTEST_SHARD_INDEX=$$i \
|
||||
LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
|
||||
./$(1) --gtest_color=yes > $(1)_shard_$$i.log 2>&1 & \
|
||||
$(SETARCH) ./$(1) --gtest_color=yes > $(1)_shard_$$i.log 2>&1 & \
|
||||
pids="$$pids $$!"; \
|
||||
done; \
|
||||
wait; \
|
||||
for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
|
||||
if ! grep -q "\[ PASSED \]" $(1)_shard_$$i.log; then \
|
||||
echo "=== Shard $$i FAILED ==="; \
|
||||
cat $(1)_shard_$$i.log; \
|
||||
fail=1; \
|
||||
else \
|
||||
passed=$$(grep "\[ PASSED \]" $(1)_shard_$$i.log); \
|
||||
exits=""; \
|
||||
for pid in $$pids; do \
|
||||
wait $$pid; exits="$$exits $$?"; \
|
||||
done; \
|
||||
i=0; \
|
||||
for ec in $$exits; do \
|
||||
log=$(1)_shard_$$i.log; \
|
||||
if grep -q "\[ PASSED \]" $$log && ! grep -q "\[ FAILED \]" $$log && [ $$ec -eq 0 ]; then \
|
||||
passed=$$(grep "\[ PASSED \]" $$log); \
|
||||
echo "Shard $$i: $$passed"; \
|
||||
else \
|
||||
echo "=== Shard $$i FAILED (exit=$$ec) ==="; \
|
||||
cat $$log; \
|
||||
fail=1; \
|
||||
fi; \
|
||||
i=$$((i+1)); \
|
||||
done; \
|
||||
if [ $$fail -ne 0 ]; then exit 1; fi; \
|
||||
echo "All shards passed."
|
||||
@@ -242,16 +251,38 @@ test_proxy_mbedtls : test_proxy.cc ../httplib.h Makefile cert.pem
|
||||
test_proxy_wolfssl : test_proxy.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS_WOLFSSL)
|
||||
|
||||
# Runs server_fuzzer.cc based on value of $(LIB_FUZZING_ENGINE).
|
||||
# Usage: make fuzz_test LIB_FUZZING_ENGINE=/path/to/libFuzzer
|
||||
fuzz_test: server_fuzzer
|
||||
./server_fuzzer fuzzing/corpus/*
|
||||
# Runs all fuzz harnesses based on the value of $(LIB_FUZZING_ENGINE).
|
||||
# By default LIB_FUZZING_ENGINE is standalone_fuzz_target_runner.o, so each
|
||||
# fuzzer is replayed over its regression corpus.
|
||||
# Override for actual fuzzing:
|
||||
# make fuzz_test LIB_FUZZING_ENGINE=/path/to/libFuzzer
|
||||
fuzz_test: server_fuzzer client_fuzzer header_parser_fuzzer url_parser_fuzzer
|
||||
@m=""; for f in fuzzing/corpus/[0-9]* fuzzing/corpus/issue1264 fuzzing/corpus/clusterfuzz-testcase-minimized-server_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
|
||||
if [ -n "$$m" ]; then echo "./server_fuzzer$$m"; ./server_fuzzer $$m; else echo "(no server_fuzzer corpus)"; fi
|
||||
@m=""; for f in fuzzing/corpus/clusterfuzz-testcase-minimized-client_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
|
||||
if [ -n "$$m" ]; then echo "./client_fuzzer$$m"; ./client_fuzzer $$m; else echo "(no client_fuzzer corpus)"; fi
|
||||
@m=""; for f in fuzzing/corpus/clusterfuzz-testcase-minimized-header_parser_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
|
||||
if [ -n "$$m" ]; then echo "./header_parser_fuzzer$$m"; ./header_parser_fuzzer $$m; else echo "(no header_parser_fuzzer corpus)"; fi
|
||||
@m=""; for f in fuzzing/corpus/clusterfuzz-testcase-minimized-url_parser_fuzzer-*; do if [ -f "$$f" ]; then m="$$m $$f"; fi; done; \
|
||||
if [ -n "$$m" ]; then echo "./url_parser_fuzzer$$m"; ./url_parser_fuzzer $$m; else echo "(no url_parser_fuzzer corpus)"; fi
|
||||
|
||||
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
||||
server_fuzzer : fuzzing/server_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
|
||||
@file $@
|
||||
|
||||
client_fuzzer : fuzzing/client_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
|
||||
@file $@
|
||||
|
||||
header_parser_fuzzer : fuzzing/header_parser_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
|
||||
@file $@
|
||||
|
||||
url_parser_fuzzer : fuzzing/url_parser_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
|
||||
@file $@
|
||||
|
||||
# Standalone fuzz runner, which just reads inputs from fuzzing/corpus/ dir and
|
||||
# feeds it to server_fuzzer.
|
||||
standalone_fuzz_target_runner.o : fuzzing/standalone_fuzz_target_runner.cpp
|
||||
@@ -264,5 +295,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 test_benchmark 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 client_fuzzer header_parser_fuzzer url_parser_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log cpp-httplib
|
||||
|
||||
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Delayed UDP responder used as a loopback test fixture.
|
||||
|
||||
This is a self-contained test fixture for the GetAddrInfoAsyncCancelTest
|
||||
cases (reproducer for cpp-httplib issue #2431). It is NOT a general-purpose
|
||||
nameserver and is only intended to run on 127.0.0.1 inside the test job's
|
||||
own runner / container.
|
||||
|
||||
What it does
|
||||
------------
|
||||
Binds a UDP socket on 127.0.0.1:<port>, accepts well-formed DNS queries
|
||||
from the test process, waits <delay_seconds>, then sends back a minimal
|
||||
NXDOMAIN reply. The deliberate delay is what makes the bug reproducible:
|
||||
|
||||
* The test calls getaddrinfo_with_timeout() with timeout_sec=1.
|
||||
* gai_suspend() returns EAI_AGAIN after 1s; the function returns and
|
||||
its stack frame is destroyed.
|
||||
* The fixture replies after <delay_seconds> (= 3s by default), so the
|
||||
glibc resolver worker thread receives the response *after* the
|
||||
caller's frame is gone and writes back into freed stack memory.
|
||||
* AddressSanitizer (with detect_stack_use_after_return=1) catches the
|
||||
write and aborts with a stack-use-after-return diagnostic.
|
||||
|
||||
Without this fixture the bug is hard to surface: dropping UDP/53 makes
|
||||
the resolver hang forever, so the worker never receives anything and
|
||||
never reaches the buggy write-back path.
|
||||
|
||||
Usage
|
||||
-----
|
||||
python3 test/dns_test_fixture.py <port> [<delay_seconds>]
|
||||
|
||||
Only standard library; no third-party dependencies.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
def serve(port: int, delay_sec: float) -> None:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(("127.0.0.1", port))
|
||||
print(
|
||||
f"[dns_test_fixture] listening on 127.0.0.1:{port}, "
|
||||
f"reply delay={delay_sec}s",
|
||||
flush=True,
|
||||
)
|
||||
while True:
|
||||
try:
|
||||
data, addr = sock.recvfrom(2048)
|
||||
except OSError:
|
||||
return
|
||||
threading.Thread(
|
||||
target=_reply_after_delay,
|
||||
args=(sock, data, addr, delay_sec),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
|
||||
def _reply_after_delay(sock, query: bytes, addr, delay_sec: float) -> None:
|
||||
time.sleep(delay_sec)
|
||||
if len(query) < 12:
|
||||
return
|
||||
# Header: copy transaction id, set QR=1 RA=1 RCODE=3 (NXDOMAIN),
|
||||
# preserve the requester's RD bit, then echo the question section so
|
||||
# glibc's resolver accepts the reply as matching its outstanding query.
|
||||
txid = query[:2]
|
||||
rd_bit = query[2] & 0x01
|
||||
flags = struct.pack(">H", 0x8003 | (rd_bit << 8))
|
||||
counts = struct.pack(">HHHH", 1, 0, 0, 0)
|
||||
question = query[12:]
|
||||
reply = txid + flags + counts + question
|
||||
try:
|
||||
sock.sendto(reply, addr)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
port_arg = int(sys.argv[1])
|
||||
delay_arg = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0
|
||||
serve(port_arg, delay_arg)
|
||||
@@ -13,7 +13,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
||||
BROTLI_DIR = /usr/local/opt/brotli
|
||||
# BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
|
||||
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer
|
||||
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer
|
||||
|
||||
# Runs all the tests and also fuzz tests against seed corpus.
|
||||
all : $(FUZZERS)
|
||||
@@ -25,6 +25,10 @@ server_fuzzer : server_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
zip -q -r server_fuzzer_seed_corpus.zip corpus
|
||||
|
||||
client_fuzzer : client_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
zip -q -r client_fuzzer_seed_corpus.zip corpus
|
||||
|
||||
header_parser_fuzzer : header_parser_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <httplib.h>
|
||||
|
||||
class FuzzedStream : public httplib::Stream {
|
||||
public:
|
||||
FuzzedStream(const uint8_t *data, size_t size)
|
||||
: data_(data), size_(size), read_pos_(0) {}
|
||||
|
||||
ssize_t read(char *ptr, size_t size) override {
|
||||
if (size + read_pos_ > size_) { size = size_ - read_pos_; }
|
||||
memcpy(ptr, data_ + read_pos_, size);
|
||||
read_pos_ += size;
|
||||
return static_cast<ssize_t>(size);
|
||||
}
|
||||
|
||||
ssize_t write(const char *ptr, size_t size) override {
|
||||
request_.append(ptr, size);
|
||||
return static_cast<ssize_t>(size);
|
||||
}
|
||||
|
||||
ssize_t write(const char *ptr) { return write(ptr, strlen(ptr)); }
|
||||
|
||||
ssize_t write(const std::string &s) { return write(s.data(), s.size()); }
|
||||
|
||||
bool is_readable() const override { return true; }
|
||||
|
||||
bool wait_readable() const override { return true; }
|
||||
|
||||
bool wait_writable() const override { return true; }
|
||||
|
||||
void get_remote_ip_and_port(std::string &ip, int &port) const override {
|
||||
ip = "127.0.0.1";
|
||||
port = 8080;
|
||||
}
|
||||
|
||||
void get_local_ip_and_port(std::string &ip, int &port) const override {
|
||||
ip = "127.0.0.1";
|
||||
port = 8080;
|
||||
}
|
||||
|
||||
socket_t socket() const override { return 0; }
|
||||
|
||||
time_t duration() const override { return 0; };
|
||||
|
||||
private:
|
||||
const uint8_t *data_;
|
||||
size_t size_;
|
||||
size_t read_pos_;
|
||||
std::string request_;
|
||||
};
|
||||
|
||||
class FuzzableClient : public httplib::ClientImpl {
|
||||
public:
|
||||
FuzzableClient() : httplib::ClientImpl("localhost", 8080) {}
|
||||
|
||||
void ProcessFuzzedResponse(FuzzedStream &stream, const std::string &method) {
|
||||
httplib::Request req;
|
||||
req.method = method;
|
||||
req.path = "/";
|
||||
httplib::Response res;
|
||||
bool close_connection = false;
|
||||
httplib::Error error = httplib::Error::Success;
|
||||
|
||||
process_request(stream, req, res, close_connection, error);
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
if (size < 1) return 0;
|
||||
|
||||
FuzzedStream stream{data + 1, size - 1};
|
||||
FuzzableClient client;
|
||||
|
||||
// Use the first byte to select method
|
||||
std::string method;
|
||||
switch (data[0] % 6) {
|
||||
case 0: method = "GET"; break;
|
||||
case 1: method = "POST"; break;
|
||||
case 2: method = "PUT"; break;
|
||||
case 3: method = "PATCH"; break;
|
||||
case 4: method = "DELETE"; break;
|
||||
case 5: method = "OPTIONS"; break;
|
||||
}
|
||||
|
||||
client.ProcessFuzzedResponse(stream, method);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
HTTP/1.1 777
|
||||
Content-Length:20000000000
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
HTTP/1.1 777
|
||||
Content-Length:446744071854775
|
||||
|
||||
BIN
Binary file not shown.
@@ -1,3 +1,7 @@
|
||||
# OpenSSL 3.x internal caches (provider, cipher, keymgmt) are allocated
|
||||
# lazily and intentionally kept until process exit. These are not real leaks.
|
||||
leak:libcrypto
|
||||
|
||||
# wolfSSL keeps ECC point/scratch buffers alive across handshakes; they are
|
||||
# released only at library shutdown which the test binaries do not invoke.
|
||||
leak:libwolfssl
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproducer runner for Issue #2431
|
||||
# (https://github.com/yhirose/cpp-httplib/issues/2431).
|
||||
#
|
||||
# Spins up an Ubuntu container, runs the loopback DNS test fixture
|
||||
# (test/dns_test_fixture.py), routes the container's DNS lookups to
|
||||
# that fixture via an iptables NAT rule, builds the test suite with
|
||||
# g++ + ASAN, and runs the GetAddrInfoAsyncCancelTest cases.
|
||||
#
|
||||
# Expected outcomes:
|
||||
# - HEAD prior to the fix: ASAN reports stack-use-after-return inside
|
||||
# getaddrinfo_with_timeout's getaddrinfo_a path during one of the
|
||||
# GetAddrInfoAsyncCancelTest cases.
|
||||
# - HEAD with the fix applied: all three cases PASS.
|
||||
#
|
||||
# Usage:
|
||||
# bash test/run_issue_2431_repro.sh
|
||||
#
|
||||
# Requirements: Docker (Linux container support). The container needs
|
||||
# --privileged because the test binary uses `setarch -R` to disable ASLR
|
||||
# for ASAN compatibility, and because the test job manages iptables
|
||||
# rules inside the container.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
docker run --rm --privileged \
|
||||
-v "$REPO_ROOT:/work" \
|
||||
-w /work/test \
|
||||
ubuntu:24.04 bash -c '
|
||||
set -euo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq --no-install-recommends \
|
||||
ca-certificates g++ make pkg-config iptables iproute2 util-linux coreutils file \
|
||||
python3 \
|
||||
libssl-dev zlib1g-dev libbrotli-dev libzstd-dev libcurl4-openssl-dev \
|
||||
>/dev/null
|
||||
|
||||
# Force DNS-only resolution: Ubuntu defaults nsswitch.conf to
|
||||
# "hosts: files mdns4_minimal [NOTFOUND=return] dns ...", which
|
||||
# short-circuits to NOTFOUND before reaching glibc DNS code, so the
|
||||
# gai_cancel() branch never gets exercised.
|
||||
sed -i "s/^hosts:.*/hosts: dns/" /etc/nsswitch.conf
|
||||
|
||||
# Start the loopback DNS test fixture (delayed UDP responder).
|
||||
DNS_FIXTURE_PORT=15353
|
||||
DNS_FIXTURE_DELAY=3
|
||||
python3 /work/test/dns_test_fixture.py "$DNS_FIXTURE_PORT" "$DNS_FIXTURE_DELAY" \
|
||||
>/tmp/dns_fixture.log 2>&1 &
|
||||
FIXTURE_PID=$!
|
||||
|
||||
# Route the container DNS lookups to the fixture; conntrack handles the
|
||||
# reply path automatically. /etc/resolv.conf is left untouched.
|
||||
iptables -t nat -I OUTPUT -p udp --dport 53 \
|
||||
-j REDIRECT --to-port "$DNS_FIXTURE_PORT"
|
||||
|
||||
trap '"'"'iptables -t nat -F OUTPUT 2>/dev/null || true; kill "$FIXTURE_PID" 2>/dev/null || true'"'"' EXIT
|
||||
|
||||
# Wait for the fixture to start listening.
|
||||
for _ in $(seq 1 50); do
|
||||
if ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
ss -lun "( sport = :$DNS_FIXTURE_PORT )" | grep -q ":$DNS_FIXTURE_PORT" || {
|
||||
echo "ERROR: dns_test_fixture failed to start" >&2
|
||||
cat /tmp/dns_fixture.log >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Sanity check: a DNS lookup must take at least the fixture delay
|
||||
# (proving the NAT rule routes the query to the fixture).
|
||||
start=$(date +%s)
|
||||
getent hosts unresolvable-host.invalid >/dev/null 2>&1 || true
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
if [ "$elapsed" -lt 2 ]; then
|
||||
echo "ERROR: lookup returned in ${elapsed}s; fixture not in DNS path" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] DNS lookups are routed to the test fixture (took ${elapsed}s)"
|
||||
|
||||
cd /work/test
|
||||
echo "=== building test binary (g++ + ASAN) ==="
|
||||
make CXX=g++ test 2>&1 | tail -5
|
||||
|
||||
ARCH=$(uname -m)
|
||||
echo "=== running GetAddrInfoAsyncCancelTest with CPPHTTPLIB_TEST_ISSUE_2431=1 ==="
|
||||
set +e
|
||||
CPPHTTPLIB_TEST_ISSUE_2431=1 \
|
||||
ASAN_OPTIONS=detect_stack_use_after_return=1 \
|
||||
setarch "$ARCH" -R \
|
||||
./test --gtest_filter="GetAddrInfoAsyncCancelTest.*" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
echo "=== test exit: $rc ==="
|
||||
exit $rc
|
||||
'
|
||||
+303
-41
@@ -1549,6 +1549,140 @@ TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(8));
|
||||
}
|
||||
|
||||
#if defined(__linux__) && defined(__GLIBC__) && \
|
||||
defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO)
|
||||
|
||||
// Forward declaration: in split builds split.py strips `inline` and moves the
|
||||
// definition into httplib.cc, so detail::getaddrinfo_with_timeout is not
|
||||
// visible from the public httplib.h. Re-declaring it here lets the tests link
|
||||
// against the symbol in both header-only and split builds.
|
||||
namespace httplib {
|
||||
namespace detail {
|
||||
int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
const struct addrinfo *hints,
|
||||
struct addrinfo **res, time_t timeout_sec);
|
||||
} // namespace detail
|
||||
} // namespace httplib
|
||||
|
||||
// Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
|
||||
//
|
||||
// On Linux/glibc, getaddrinfo_with_timeout() runs the lookup via
|
||||
// getaddrinfo_a(GAI_NOWAIT) using a stack-local `struct gaicb`. When the
|
||||
// gai_suspend() call hits the connection timeout the function calls
|
||||
// gai_cancel() and returns immediately. gai_cancel() is non-blocking and
|
||||
// can return EAI_NOTCANCELED, in which case the resolver worker thread is
|
||||
// still alive and still references the now-destroyed stack frame.
|
||||
//
|
||||
// Triggering the bug requires DNS to actually hang (UDP/53 dropped, etc.),
|
||||
// so these tests are gated on CPPHTTPLIB_TEST_ISSUE_2431=1 and are skipped
|
||||
// during normal runs. test/run_issue_2431_repro.sh sets up the environment
|
||||
// and runs them in a container.
|
||||
namespace {
|
||||
bool should_run_issue_2431_tests() {
|
||||
const char *v = getenv("CPPHTTPLIB_TEST_ISSUE_2431");
|
||||
return v && *v && std::string(v) != "0";
|
||||
}
|
||||
|
||||
std::string unique_unresolvable_host(int n) {
|
||||
// .invalid is reserved (RFC 6761) and is never served by real DNS, but
|
||||
// glibc still asks the configured nameserver — which is exactly the path
|
||||
// we want to exercise. A unique label per call avoids the resolver cache.
|
||||
auto t = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
return "h-" + std::to_string(::getpid()) + "-" + std::to_string(t) + "-" +
|
||||
std::to_string(n) + ".invalid";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(GetAddrInfoAsyncCancelTest, DirectCallSingleThread) {
|
||||
if (!should_run_issue_2431_tests()) {
|
||||
GTEST_SKIP()
|
||||
<< "Set CPPHTTPLIB_TEST_ISSUE_2431=1 (and sinkhole DNS) to run";
|
||||
}
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
struct addrinfo hints;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
auto host = unique_unresolvable_host(i);
|
||||
struct addrinfo *result = nullptr;
|
||||
int rc = detail::getaddrinfo_with_timeout(host.c_str(), "80", &hints,
|
||||
&result, /*timeout_sec=*/1);
|
||||
if (rc == 0 && result) { freeaddrinfo(result); }
|
||||
}
|
||||
|
||||
// Give orphaned getaddrinfo_a worker threads a chance to write into the
|
||||
// stack region they still believe holds their gaicb.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
}
|
||||
|
||||
TEST(GetAddrInfoAsyncCancelTest, DirectCallMultiThread) {
|
||||
if (!should_run_issue_2431_tests()) {
|
||||
GTEST_SKIP()
|
||||
<< "Set CPPHTTPLIB_TEST_ISSUE_2431=1 (and sinkhole DNS) to run";
|
||||
}
|
||||
|
||||
std::atomic<bool> stop{false};
|
||||
std::vector<std::thread> threads;
|
||||
for (int t = 0; t < 8; ++t) {
|
||||
threads.emplace_back([t, &stop] {
|
||||
int i = 0;
|
||||
while (!stop.load(std::memory_order_relaxed)) {
|
||||
struct addrinfo hints;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
auto host = unique_unresolvable_host(t * 100000 + i++);
|
||||
struct addrinfo *result = nullptr;
|
||||
int rc = detail::getaddrinfo_with_timeout(host.c_str(), "80", &hints,
|
||||
&result, /*timeout_sec=*/1);
|
||||
if (rc == 0 && result) { freeaddrinfo(result); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::seconds(8));
|
||||
stop.store(true, std::memory_order_relaxed);
|
||||
for (auto &th : threads) {
|
||||
th.join();
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
}
|
||||
|
||||
TEST(GetAddrInfoAsyncCancelTest, ClientGetMultiThread) {
|
||||
if (!should_run_issue_2431_tests()) {
|
||||
GTEST_SKIP()
|
||||
<< "Set CPPHTTPLIB_TEST_ISSUE_2431=1 (and sinkhole DNS) to run";
|
||||
}
|
||||
|
||||
std::atomic<bool> stop{false};
|
||||
std::vector<std::thread> threads;
|
||||
for (int t = 0; t < 8; ++t) {
|
||||
threads.emplace_back([t, &stop] {
|
||||
int i = 0;
|
||||
while (!stop.load(std::memory_order_relaxed)) {
|
||||
auto host = unique_unresolvable_host(t * 100000 + i++);
|
||||
Client cli(host, 80);
|
||||
cli.set_connection_timeout(1, 0);
|
||||
cli.set_read_timeout(1, 0);
|
||||
cli.set_write_timeout(1, 0);
|
||||
(void)cli.Get("/");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::seconds(8));
|
||||
stop.store(true, std::memory_order_relaxed);
|
||||
for (auto &th : threads) {
|
||||
th.join();
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
}
|
||||
|
||||
#endif // __linux__ && __GLIBC__ && CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO
|
||||
|
||||
TEST(ConnectionErrorTest, InvalidHost) {
|
||||
auto host = "-abcde.com";
|
||||
|
||||
@@ -3390,7 +3524,7 @@ protected:
|
||||
})
|
||||
.Get("/slow",
|
||||
[&](const Request & /*req*/, Response &res) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(4));
|
||||
res.set_content("slow", "text/plain");
|
||||
})
|
||||
#if 0
|
||||
@@ -6909,7 +7043,7 @@ TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) {
|
||||
EXPECT_EQ(StatusCode::UriTooLong_414, res.status);
|
||||
EXPECT_EQ("close", res.get_header_value("Connection"));
|
||||
EXPECT_FALSE(cli_.is_socket_open());
|
||||
EXPECT_LE(elapsed, 200);
|
||||
EXPECT_LE(elapsed, 2000);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -7805,6 +7939,31 @@ TEST(MountTest, MultibytesPathName) {
|
||||
EXPECT_EQ(U8("日本語コンテンツ"), res->body);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Issue #2435: mmap::open() must succeed even when another handle holds
|
||||
// the file open for writing (e.g. an active log file).
|
||||
TEST(MmapTest, OpenWhileFileHeldForWriting) {
|
||||
const char *path = "mmap_concurrent_writer_test.txt";
|
||||
const char *content = "hello";
|
||||
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
f.write(content, static_cast<std::streamsize>(strlen(content)));
|
||||
}
|
||||
auto file_cleanup = detail::scope_exit([&] { std::remove(path); });
|
||||
|
||||
HANDLE writer = ::CreateFileA(path, GENERIC_WRITE, FILE_SHARE_READ, NULL,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
ASSERT_NE(INVALID_HANDLE_VALUE, writer);
|
||||
auto handle_cleanup = detail::scope_exit([&] { ::CloseHandle(writer); });
|
||||
|
||||
detail::mmap m(path);
|
||||
ASSERT_TRUE(m.is_open());
|
||||
EXPECT_EQ(strlen(content), m.size());
|
||||
EXPECT_EQ(0, std::memcmp(content, m.data(), strlen(content)));
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(KeepAliveTest, ReadTimeout) {
|
||||
Server svr;
|
||||
|
||||
@@ -9129,6 +9288,81 @@ TEST(ClientVulnerabilityTest, PayloadMaxLengthZeroMeansNoLimit) {
|
||||
<< " bytes without truncation, but only read " << total_read << " bytes.";
|
||||
}
|
||||
|
||||
// Regression test for OSS-Fuzz issue 508342856: a malicious server sending an
|
||||
// enormous Content-Length must not cause the client to pre-allocate a huge
|
||||
// response body buffer. The reservation is capped at payload_max_length_, and
|
||||
// the read itself fails when the body exceeds the limit.
|
||||
TEST(ClientVulnerabilityTest, HugeContentLengthDoesNotPreallocate) {
|
||||
#ifndef _WIN32
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
auto server_thread = std::thread([] {
|
||||
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 + 2));
|
||||
::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);
|
||||
|
||||
// Malicious response: claim a 20GB body but send only a tiny payload.
|
||||
std::string response = "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Length: 20000000000\r\n"
|
||||
"\r\n"
|
||||
"abc";
|
||||
::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);
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
{
|
||||
Client cli("127.0.0.1", PORT + 2);
|
||||
cli.set_read_timeout(5, 0);
|
||||
// Default payload_max_length_ is 100MB; a 20GB Content-Length must not
|
||||
// result in a 20GB pre-allocation. The Get() call is expected to fail
|
||||
// (server claims more bytes than payload_max_length permits), but it must
|
||||
// not exhaust memory before getting there.
|
||||
auto res = cli.Get("/malicious");
|
||||
EXPECT_FALSE(res); // Read fails because body exceeds payload_max_length_
|
||||
}
|
||||
|
||||
server_thread.join();
|
||||
}
|
||||
|
||||
// Verify that content_receiver bypasses the default payload_max_length,
|
||||
// allowing streaming downloads larger than 100MB without requiring an explicit
|
||||
// set_payload_max_length call.
|
||||
@@ -12142,7 +12376,7 @@ TEST(MultipartFormDataTest, ManyItemsEndToEnd) {
|
||||
TEST(MultipartFormDataTest, MakeFileProvider) {
|
||||
// Verify make_file_provider sends a file's contents correctly.
|
||||
const std::string file_content(4096, 'Z');
|
||||
const std::string tmp_path = "/tmp/httplib_test_make_file_provider.bin";
|
||||
const std::string tmp_path = "./httplib_test_make_file_provider.bin";
|
||||
{
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
ofs.write(file_content.data(),
|
||||
@@ -12197,7 +12431,7 @@ TEST(MultipartFormDataTest, MakeFileProvider) {
|
||||
|
||||
TEST(MakeFileBodyTest, Basic) {
|
||||
const std::string file_content(4096, 'Z');
|
||||
const std::string tmp_path = "/tmp/httplib_test_make_file_body.bin";
|
||||
const std::string tmp_path = "./httplib_test_make_file_body.bin";
|
||||
{
|
||||
std::ofstream ofs(tmp_path, std::ios::binary);
|
||||
ofs.write(file_content.data(),
|
||||
@@ -14275,8 +14509,23 @@ static std::thread serve_single_response(std::promise<int> &port_promise,
|
||||
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
|
||||
|
||||
if (cli != INVALID_SOCKET) {
|
||||
char buf[4096];
|
||||
::recv(cli, buf, sizeof(buf), 0);
|
||||
// Read the complete HTTP request (until the blank line that terminates
|
||||
// headers) before sending the response. If the server closes the socket
|
||||
// while the client's request data is still unread in the kernel receive
|
||||
// buffer, the TCP stack sends RST instead of FIN. That RST resets the
|
||||
// connection on both sides: it discards the client's receive buffer
|
||||
// (which already holds our response) and causes any in-progress write on
|
||||
// the client to fail with ECONNRESET. Reading all request data first
|
||||
// ensures close() emits a graceful FIN.
|
||||
{
|
||||
char rbuf[256];
|
||||
std::string req;
|
||||
while (req.find("\r\n\r\n") == std::string::npos) {
|
||||
auto n = ::recv(cli, rbuf, sizeof(rbuf), 0);
|
||||
if (n <= 0) { break; }
|
||||
req.append(rbuf, static_cast<size_t>(n));
|
||||
}
|
||||
}
|
||||
|
||||
::send(cli,
|
||||
#ifdef _WIN32
|
||||
@@ -14471,6 +14720,48 @@ TEST_F(SSLOpenStreamTest, PostChunked) {
|
||||
auto body = read_all(handle);
|
||||
EXPECT_EQ("Chunked SSL Data", body);
|
||||
}
|
||||
|
||||
// RFC 7230 §3.3: a response body with neither chunked Transfer-Encoding nor
|
||||
// Content-Length is terminated by the server closing the connection. When the
|
||||
// SSL peer sends a close_notify after the body, the client must treat it as a
|
||||
// clean EOF and return a successful response rather than an error.
|
||||
TEST(SSLTest, ResponseBodyTerminatedByConnectionClose) {
|
||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
|
||||
svr.set_keep_alive_max_count(1);
|
||||
|
||||
const std::string expected_body = "Hello from connection-close response!";
|
||||
|
||||
svr.Get("/no-content-length", [&](const Request & /*req*/, Response &res) {
|
||||
res.set_content_provider("text/plain",
|
||||
[&](size_t offset, DataSink &sink) -> bool {
|
||||
if (offset < expected_body.size()) {
|
||||
sink.write(expected_body.data() + offset,
|
||||
expected_body.size() - offset);
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&svr] { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
svr.wait_until_ready();
|
||||
|
||||
SSLClient cli(HOST, PORT);
|
||||
cli.enable_server_certificate_verification(false);
|
||||
|
||||
auto res = cli.Get("/no-content-length");
|
||||
|
||||
ASSERT_TRUE(res) << "Request failed: " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ(expected_body, res->body);
|
||||
}
|
||||
#endif // CPPHTTPLIB_SSL_ENABLED
|
||||
|
||||
//==============================================================================
|
||||
@@ -16587,41 +16878,6 @@ TEST(BindServerTest, UpdateCerts) {
|
||||
EVP_PKEY_free(key);
|
||||
}
|
||||
|
||||
// Test that SSLServer(X509*, EVP_PKEY*, X509_STORE*) constructor sets
|
||||
// client CA list correctly for TLS handshake
|
||||
TEST(SSLClientServerTest, X509ConstructorSetsClientCAList) {
|
||||
X509 *cert = readCertificate(SERVER_CERT_FILE);
|
||||
X509 *ca_cert = readCertificate(CLIENT_CA_CERT_FILE);
|
||||
EVP_PKEY *key = readPrivateKey(SERVER_PRIVATE_KEY_FILE);
|
||||
|
||||
ASSERT_TRUE(cert != nullptr);
|
||||
ASSERT_TRUE(ca_cert != nullptr);
|
||||
ASSERT_TRUE(key != nullptr);
|
||||
|
||||
X509_STORE *cert_store = X509_STORE_new();
|
||||
X509_STORE_add_cert(cert_store, ca_cert);
|
||||
|
||||
// Use X509-based constructor (deprecated but should still work correctly)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
SSLServer svr(cert, key, cert_store);
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
|
||||
// Verify that client CA list is set in SSL_CTX
|
||||
auto ssl_ctx = static_cast<SSL_CTX *>(svr.tls_context());
|
||||
ASSERT_TRUE(ssl_ctx != nullptr);
|
||||
|
||||
STACK_OF(X509_NAME) *ca_list = SSL_CTX_get_client_CA_list(ssl_ctx);
|
||||
ASSERT_TRUE(ca_list != nullptr);
|
||||
EXPECT_GT(sk_X509_NAME_num(ca_list), 0);
|
||||
|
||||
X509_free(cert);
|
||||
X509_free(ca_cert);
|
||||
EVP_PKEY_free(key);
|
||||
}
|
||||
|
||||
// Test that update_certs() updates client CA list correctly
|
||||
TEST(SSLClientServerTest, UpdateCertsSetsClientCAList) {
|
||||
// Start with file-based constructor
|
||||
@@ -17575,8 +17831,14 @@ TEST(SymlinkTest, SymlinkEscapeFromBaseDirectory) {
|
||||
}
|
||||
|
||||
// Create symlink using absolute path so it resolves correctly
|
||||
#ifdef PATH_MAX
|
||||
char abs_secret[PATH_MAX];
|
||||
ASSERT_NE(nullptr, realpath(secret_dir.c_str(), abs_secret));
|
||||
#else
|
||||
auto abs_secret = realpath(secret_dir.c_str(), nullptr);
|
||||
auto abs_secret_guard = detail::scope_exit([&]() { std::free(abs_secret); });
|
||||
ASSERT_NE(nullptr, abs_secret);
|
||||
#endif
|
||||
ASSERT_EQ(0, symlink(abs_secret, symlink_path.c_str()));
|
||||
|
||||
auto se = detail::scope_exit([&] {
|
||||
|
||||
@@ -136,6 +136,86 @@ TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
|
||||
client.close();
|
||||
}
|
||||
|
||||
// Verify that the client detects a non-responsive peer via unacked-ping count.
|
||||
// Setup: the server's heartbeat is disabled AND its handler never calls
|
||||
// read(), so no automatic Pong reply is ever produced. The client sends
|
||||
// pings but receives no pongs, and should close itself once the unacked
|
||||
// ping count reaches max_missed_pongs.
|
||||
class WebSocketPongTimeoutTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
svr_.set_websocket_ping_interval(0);
|
||||
svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &) {
|
||||
std::unique_lock<std::mutex> lock(handler_mutex_);
|
||||
handler_cv_.wait(lock, [this]() { return release_; });
|
||||
});
|
||||
|
||||
port_ = svr_.bind_to_any_port("localhost");
|
||||
thread_ = std::thread([this]() { svr_.listen_after_bind(); });
|
||||
svr_.wait_until_ready();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(handler_mutex_);
|
||||
release_ = true;
|
||||
}
|
||||
handler_cv_.notify_all();
|
||||
svr_.stop();
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
Server svr_;
|
||||
int port_;
|
||||
std::thread thread_;
|
||||
std::mutex handler_mutex_;
|
||||
std::condition_variable handler_cv_;
|
||||
bool release_ = false;
|
||||
};
|
||||
|
||||
TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
client.set_websocket_max_missed_pongs(2);
|
||||
ASSERT_TRUE(client.connect());
|
||||
ASSERT_TRUE(client.is_open());
|
||||
|
||||
// Client pings every 1s (compile-time default in this test file).
|
||||
// With max_missed_pongs = 2, the heartbeat thread should self-close within
|
||||
// roughly 3s. Poll is_open() up to 6s.
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (client.is_open() &&
|
||||
std::chrono::steady_clock::now() - start < std::chrono::seconds(6)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
EXPECT_FALSE(client.is_open());
|
||||
}
|
||||
|
||||
// Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
|
||||
// even with a small max_missed_pongs budget. This is the positive counterpart
|
||||
// of ClientDetectsNonResponsivePeer: the client must actively drive read() so
|
||||
// that incoming Pong frames are consumed and the unacked counter is reset.
|
||||
TEST_F(WebSocketHeartbeatTest, ResponsivePeerNeverTimesOut) {
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
client.set_websocket_max_missed_pongs(2);
|
||||
ASSERT_TRUE(client.connect());
|
||||
|
||||
// Interactive loop over ~6s, longer than 2 ping intervals, so the
|
||||
// pong-timeout mechanism would trigger if pongs weren't being consumed.
|
||||
// Each iteration's read() also drains any pending Pong frame.
|
||||
for (int i = 0; i < 6; i++) {
|
||||
std::string text = "keepalive" + std::to_string(i);
|
||||
ASSERT_TRUE(client.send(text));
|
||||
std::string msg;
|
||||
ASSERT_TRUE(client.read(msg));
|
||||
EXPECT_EQ(text, msg);
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}
|
||||
|
||||
EXPECT_TRUE(client.is_open());
|
||||
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