Compare commits

..
39 Commits
Author SHA1 Message Date
Wleeaf 252dbaa505 Remove dot segments from absolute redirect locations (#2648)
resolve_relative_location removed "." and ".." segments only from a relative-path Location. A Location that starts with "/" or an absolute URI was requested as is, so a redirect to /dir/../target went out as /dir/../target and a server that does not normalize the path answered 404.

RFC 3986 section 5.2.2 applies remove_dot_segments to the target path of every reference that has a path, and Go and urllib3 do the same. The loop now lives in detail::remove_dot_segments, and ClientImpl::redirect applies it to an absolute path as well. Relative locations resolve as before.
2026-10-25 22:56:32 -04:00
metsw24-max 8710fbd25d Refuse a ':' in a path a mount point serves on Windows (#2643)
On NTFS a ':' in a path names a stream of the file, so /dir/test.html::$DATA opened the default stream of dir/test.html and passed a set_pre_routing_handler() guard on /dir/test.html, and /setup.exe:Zone.Identifier served the stream Windows attaches to downloaded files. is_valid_path() now refuses a ':' on Windows.

Elsewhere a ':' is an ordinary file name character, so it is still served: a file such as 2024-01-01T10:00:00.log would otherwise answer 404. The test pins both sides.
2026-10-25 22:39:44 -04:00
metsw24-max 2f8b9f6f57 Guard last_event_id() against the async event loop in SSEClient (#2644)
last_event_id() returned a reference to a string that run_event_loop() assigns on its own thread under start_async(), so a caller reading it from another thread raced the writes. The member is now guarded by a mutex and the getter returns a copy.

The return type changes from const std::string & to std::string. Code that binds the result to a const reference or copies it still compiles.
2026-10-25 22:21:55 -04:00
metsw24-maxandyhirose 391de32115 finish short writes in write_websocket_frame and open_stream_ (#2642)
write_websocket_frame and the request-body write in open_stream_ called
Stream::write once and only failed on a negative return, so a short
write (one record per mbedtls_ssl_write call, or SO_SNDTIMEO expiring
after part of the data went out) dropped the rest of the frame or body.
Route both through write_data, as the other body writers do.

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-10-25 22:19:53 -04:00
metsw24-max ba32b468a4 Honour Keychain trust settings when loading macOS system CAs (#2641)
The macOS system CA loader took every certificate that has Trust Settings, in the system, admin or user domain, as a trusted root without reading those settings. A root set to "Never Trust" in Keychain Access, or trusted only for S/MIME or code signing, still verified TLS servers.

The loader now evaluates the settings for the SSL policy. A per-user setting overrides the admin domain, which overrides the system domain, and only TrustRoot and TrustAsRoot are loaded. The OpenSSL path shares the helper that the Mbed TLS and wolfSSL paths already use.
2026-10-25 21:41:36 -04:00
Luochenchen 76f84a5bfd Honor set_decompress(false) in response streams (#2640)
open_stream_() created a decoder whatever set_decompress() said, so a response stream decoded the body when decompression was disabled, and failed with UnsupportedContentEncoding in a build without that decoder. The buffered path already checks the option; the streaming path now does too. The new test covers open_stream() and stream::Get().

Fixes #2639.
2026-10-25 20:05:56 -04:00
yhirose d23d1b6029 Match a host name against subjectAltName only
OpenSSL 4.1 no longer falls back to the subject Common Name when it
checks a host name (RFC 9525), so SSLClientServerTest.TlsVerifyHostname
began failing in the Windows CI job that installs the newest OpenSSL 4.x.

Make every backend agree instead of following whichever OpenSSL is
installed. OpenSSL passes X509_CHECK_FLAG_NEVER_CHECK_SUBJECT to both the
handshake check and verify_hostname(), and Mbed TLS and wolfSSL drop their
own CN fallback. A certificate that names its host only in the CN is now
rejected on all backends, as it already was on BoringSSL.
2026-10-25 18:58:43 -04:00
Wleeaf 7d349cbeed Follow only 301, 302, 303, 307 and 308 redirects (#2646)
is_redirect treated any 3xx with a Location as a redirect, so with set_follow_location(true) a 304 or 305 that carried a Location was followed. Only 301, 302, 303, 307 and 308 are redirects a client follows (RFC 9110 section 15.4), as in Go, requests and urllib3. A Location is still required. The 304 check in process_request that is_redirect now covers is dropped.
2026-10-25 18:55:20 -04:00
WleeafandWleeaf 5ceef83c54 Open a new connection for a redirect after sending Connection: close (#2645)
Without keep-alive, the client sends "Connection: close", but before
following a redirect it disconnected only when the response repeated
the option. RFC 9112 (section 9.6) lets a server close the connection
without repeating it, and a client that sent "close" must not send
another request on that connection. The redirected request was written
to the closing connection and failed with "Failed to read connection"
whenever the server closed it first. The client now disconnects before
a redirect or an auth retry whenever the request asked for the close.

Co-authored-by: Wleeaf <179125857+wleeaf@users.noreply.github.com>
2026-10-25 17:19:34 -04:00
yhirose a210b43e11 Give the ARM cross-compile job's build step its own time limit
apt on the runner mirror sometimes crawls for minutes
(actions/runner-images#14823), and the cross compiler is large enough
that its install alone used up the job's 10-minute limit. Loosen the job
limit to 30 minutes and limit the build step, which normally takes a few
seconds, to 2 minutes.
2026-10-22 18:00:44 -04:00
yhirose 7d28b64a03 Build BoringSSL with clang in the Linux CI job
GCC 15 on ubuntu-latest (26.04) reports a false -Wstringop-overflow in
BoringSSL's Keccak code, and BoringSSL's -Werror makes the build fail.
2026-10-22 17:40:53 -04:00
yhirose 97a972fcef Give the issue-2431 job's test step its own time limit
apt on the runner mirror sometimes crawls for minutes
(actions/runner-images#14823), which used up the job's 5-minute limit
before the test finished. Loosen the job limit to 15 minutes and limit
the test step, which normally takes about 80s, to 3 minutes.
2026-10-22 17:40:53 -04:00
metsw24-max 276cf08e84 Refuse a proxied path that does not start with a slash (#2638)
In absolute-form the path follows the authority, so a path such as "@evil.example/x" made the proxy connect to another host and forward the origin's credentials. Both handle_request and open_stream_ now refuse such a path before anything is written.
2026-10-22 17:01:58 -04:00
yhirose b4b24589ad Refuse to serve a path segment ending in a dot or a space
Windows trims a trailing '.' or space from a file name, so a mount
point there served a file under a path other than the one a pre-routing
handler checked. A mount point now serves no path with a segment
ending in '.' or a space, on every platform; "." and ".." are among
them. The README no longer lists such names among the ones the file
system can still alias.
2026-10-22 11:16:27 -04:00
yhirose 70bed02bc9 Bound how much of an unread request body the server skips
After answering, with a status below 400, a request whose body the
handler left unread, the server read past the body to reuse the
connection up to the payload limit (100MB by default), with only each
read bounded by the read timeout, so a slow sender could hold a worker
for a long time. It now skips at most
CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH (256KB, as Go's net/http does)
within the read timeout as a whole, and closes the connection
otherwise. An error response already closed the connection.
2026-10-22 10:59:57 -04:00
yhirose 5db950e1e1 Normalize the extension of a user MIME type mapping when it is set
set_file_extension_and_mimetype_mapping() kept the extension as given,
and a file's extension was looked up as it was, then in lowercase, so a
mapping given as ".pdf" never matched and one given as "PDF" matched
only files ending in ".PDF". The extension is now lowercased and loses
a leading '.' when it is set, as Apache's AddType does, and a file's
extension is looked up in lowercase only.
2026-10-22 10:21:06 -04:00
yhirose b8ae3e43d5 Answer 500 to an exception the exception handler gives no status
An exception handler that set no status sent its response with 200, or
with whatever status the throwing handler had set. The status is now
set to 500 before the exception handler runs, as ASP.NET Core does, so
it stays 500 unless the exception handler sets another one.

On a WebSocket upgrade, an exception from the pre-routing handler, the
pre-request handler or the sub-protocol selector skipped the exception
handler and dropped the connection without a response. Everything
before the protocol switches now goes through the same exception
handling as routing, so it is answered over HTTP. The WebSocket handler
itself still runs outside it.
2026-10-22 10:01:48 -04:00
yhirose 64b5c68a46 Run RequestWithoutContentLengthOrTransferEncoding on Windows too
The test was kept off Windows while reading the body of such a request
waited on the socket and could hang there. That wait was made
non-blocking soon after, and such a request no longer has its body
read at all, so nothing in it is specific to the platform any more.
2026-10-22 09:32:29 -04:00
yhirose 1c91d54b7c Read no body from an unframed request in builds without TLS too
A request with neither Content-Length nor chunked Transfer-Encoding
has no body (RFC 9112 6.3), and builds with TLS support read none.
Builds without it still read whatever followed such a request on a
connection about to close, up to EOF, as its body. Every build now
reads no body. Tests that exercised only that read, or passed whatever
the server did, are removed, and a new one checks in every build that
the bytes after such a request are not taken as its body.
2026-10-22 08:50:17 -04:00
yhirose fbd3e2cc9b Answer 501 to a request with a transfer coding besides chunked
A request whose Transfer-Encoding ended in chunked but listed more
(another coding before chunked, or chunked twice) was read with only
the chunked framing removed, so the handler got a body still in the
other coding. RFC 9112 6.1 lets a server answer 501 to a transfer
coding it does not implement. The server now accepts only a single
Transfer-Encoding field whose value is chunked, as Go and nginx do,
and answers any other list ending in chunked with 501 and closes the
connection. A list whose final coding is not chunked still gets 400.
2026-10-22 07:58:28 -04:00
yhirose 32a0ca8b35 Ignore Range on requests other than GET and HEAD
RFC 9110 14.2 defines range handling only for GET, and a server must
ignore a Range header on any other method. The server parsed it on
every method, so a POST with a Range got a 206 slice of the handler's
response, or a 416 when the Range was malformed. Range is now parsed
only for GET and HEAD; other methods get the whole response, and a
handler can still read the header itself.
2026-10-22 07:31:50 -04:00
yhirose b83b385ed4 Match trusted proxies as IP addresses
set_trusted_proxies() compared addresses as strings, so a proxy listed
as 10.0.0.2 was not trusted when a server listening on both IPv4 and
IPv6 saw it as ::ffff:10.0.0.2, and two spellings of one IPv6 address
did not match either. The listed addresses are now put in one spelling
when they are set, and so are the connection peer and each
X-Forwarded-For hop before they are looked up; an IPv4-mapped IPv6
address becomes its IPv4 address. The README now documents the setter.
2026-10-22 00:37:09 -04:00
yhirose f297616584 Note that TLS settings do not pass through a plain HTTP redirect hop
The README said a redirect keeps the TLS verification settings. They
pass only from one HTTPS hop to the next; an HTTPS hop after a plain
HTTP one starts over with the defaults.
2026-10-22 00:10:57 -04:00
yhirose 1e35f39675 Send the chunk overhead test requests whole before they are refused
RejectsChunkOverheadPastItsLimit sent about 78 KB per case, and the
server refused the request a few chunks in and closed the connection
while the client was still writing, so the send failed now and then
instead of returning the 400. Pad each size line with a sixteenth of
the limit, so each request is about 10 KB and goes past the limit only
at its last chunks.
2026-10-22 00:10:34 -04:00
yhirose e121385910 Reconnect after a TLS setting changes on a kept-alive client
TLS settings take effect as a connection is set up, so after a client
changed one (server certificate or host name verification, a CA store,
a verifier) a later request still went over the connection kept alive
under the old settings, unverified if verification had just been turned
on. Each such setter now bumps a version that the connection records
when it is set up, and a connection set up under an older version is
closed instead of reused. The setters take no lock, so they cannot wait
on a handshake in flight.
2026-10-22 00:08:44 -04:00
yhirose fc37d3eae1 Add Server::set_compress() to turn off handler response compression
A server built with compression support compressed every handler
response the client accepted, with no way to turn it off. Over HTTPS
the length of a compressed response can give away a secret in it when
the same response also carries text an attacker chooses (BREACH).
set_compress(false) sends handler responses, buffered or streamed, as
they are; compression stays on by default, and static file compression
keeps its own setting. The README and the cookbook describe the switch
and when to use it.
2026-10-21 23:34:48 -04:00
yhirose 3b8b9cf9d5 Parse a CA store PEM through a NUL-terminated copy on Mbed TLS
tls::create_ca_store() on Mbed TLS parsed the caller's buffer with one
byte more than its length, so it read past the end of the buffer, and
when that byte was not a NUL the PEM was not recognized and the call
returned NULL. Parse a NUL-terminated copy instead, as load_ca_pem()
does. DER input still parses, since its own length ends it.
2026-10-21 23:28:19 -04:00
yhirose 3599533c24 Drop invalid response header fields before any header writer
The server left response header fields with an invalid name or value,
such as a value carrying CR/LF that would split the response, for the
default header writer to skip, so a writer set with set_header_writer()
wrote them. Drop such fields from res.headers right after the handlers
that can fill it directly have run, before the server reads it for
framing and range decisions and before any header writer sees it. The
client already rejects invalid request headers before writing them.
2026-10-21 23:16:45 -04:00
yhirose 247a2abe4a Bound the bytes around chunk data in chunked transfer coding
The payload limit counts only chunk data, and a chunk-size line is only
bounded by the line length limit, so a peer could send a few bytes of
data with any amount of padding, leading zeros and chunk extensions
around them. Count those bytes as Go does: each chunk may spend 16
bytes plus twice its data on its size line and the CRLF after it, which
still lets a stream send a byte at a time, and the excess may build up
to CPPHTTPLIB_HEADER_MAX_LENGTH before the read fails. This covers
request bodies on the server and response bodies on the client.
2026-10-21 23:01:39 -04:00
yhirose 5bcfea7b90 Release v0.63.0 2026-10-21 22:26:15 -04:00
yhirose fd71c11e0e Verify the server certificate during the handshake on OpenSSL
The OpenSSL client finished the handshake with SSL_VERIFY_NONE and
checked the server certificate only afterwards, so it presented its
client certificate to a server that then failed verification. With a
server certificate verifier that accepted, wolfSSL did the same. Verify
the chain, and the host name, during the handshake on every backend,
keeping the built-in verdict when a verifier runs alongside it. On
OpenSSL a session verifier, which may accept a certificate the built-in
check rejects, still decides after the handshake.

A certificate that fails the built-in verification now reports
CertVerifyFailed, or HostnameMismatch for its name, in ssl_error() on
every backend, where OpenSSL left it 0.
2026-10-21 22:13:22 -04:00
yhirose fd5dd79bd2 Ready PSA Crypto before parsing a CA store on Mbed TLS 4.x
tls::create_ca_store() parses certificates, which on Mbed TLS 4.x needs
PSA Crypto. A context readies it when it is created, but a store can be
made before any context exists, and then a valid PEM failed to parse
and the call returned NULL. Ready PSA Crypto first, as the contexts do.
2026-10-21 17:16:11 -04:00
yhirose 6304fc6abb Keep the system CAs out when a custom CA comes through empty
Under the default system CA policy, a custom CA given to a client keeps
the system CAs out. SSLClient and WebSocketClient only noted the custom
CA when set_ca_cert_store() got a store or load_ca_cert_store() got some
bytes, so a NULL store, such as the one tls::create_ca_store() returns
for a PEM it cannot parse, or an empty PEM left the client trusting
every system CA instead. Note the custom CA on every call, so that such
a client fails verification. A redirect still passes a CA on only when
the original client had one.
2026-10-21 17:11:52 -04:00
yhirose 8772d10c90 Serve only canonical paths from mount points
A mount point left ".", ".." and empty path segments for the file
system to resolve, while the pre-routing handler, where the README has
applications guard static files, saw the path as it was sent. A file
could then be reached under a spelling that a check on req.path did not
match. Serve a path from a mount point only when it has none of those
segments other than a trailing '/', so that the file served is the one
req.path names; any other path now gets 404. The README also notes the
other names a file system can give a file.
2026-10-21 16:55:43 -04:00
yhirose 98647e059d Require client certificates once update_certs_pem() loads a client CA
SSLServer::update_certs_pem() loaded the client CA it was given but
left client certificate verification as it was, so on a server built
without a client CA it returned true while clients without a
certificate still got in. Turn verification on, as the constructors do
when they are given a client CA. A client CA passed to
update_certs_pem() now always makes client certificates required.
2026-10-21 16:41:12 -04:00
yhirose 7a9cee0e05 Drop the SNI of a failed handshake on the Mbed TLS server
The Mbed TLS server keeps the SNI from the ClientHello in a
thread-local until the handshake succeeds, and its SNI callback only
runs for a ClientHello that carries one. A handshake that failed left
its SNI behind, so the next connection served by the same thread
without SNI reported the earlier client's name in req.sni(). Start
each handshake with the thread-local cleared.
2026-10-21 16:29:56 -04:00
yhirose 71b5f0a1c8 Read a NUL-terminated client CA in SSLServer::PemMemory
SSLServer::PemMemory takes a length of 0 to mean a NUL-terminated
string for cert_pem and key_pem, but skipped client_ca_pem when its
length was 0, so a server given its client CA that way started without
requiring client certificates. Read the client CA under the same rule
as the other fields. An empty client CA now fails to load instead of
turning the check off, and the README documents the length-0 rule.
2026-10-21 16:19:15 -04:00
metsw24-max 67b88030e8 End the stream at the header section for HEAD, 204 and 304 in open_stream (#2636)
ClientImpl::open_stream built the body reader from Content-Length and Transfer-Encoding whatever the method or status, so a stream on a response that RFC 9112 6.3 ends at its header section was read on: with a keep-alive server read() blocked until the read timeout, and when the server closed it reported a read error for a complete response. stream::Head() against cpp-httplib's own server, which puts the GET body's Content-Length on a HEAD response, hit it every time.

open_stream_() now returns with the body reader at its end for a response to HEAD and for a 204 or 304, as the buffered path skips the body for them. Content-Encoding is skipped too, so a coding the build lacks no longer fails such a stream.
2026-10-20 17:31:45 -04:00
metsw24-max 41c85c0733 Close the connection on an answered upgrade body in process_request (#2637)
When pre_routing_handler or pre_request_handler answers a WebSocket upgrade request, Server::process_request returns before the drain of an unread body and closes the connection only for a 4xx/5xx. A 2xx/3xx answer to an upgrade request with a framed body left that body to be parsed as the next request on the keep-alive connection. Those two early returns now close the connection when the request has a framed body, as the Expect rejection already does.
2026-10-20 17:08:21 -04:00
12 changed files with 2131 additions and 792 deletions

No files matched your search

+4 -1
View File
@@ -28,10 +28,13 @@ jobs:
test-arm32:
name: ARM 32-bit (cross-compile)
runs-on: ubuntu-latest
timeout-minutes: 10
# Loose, because apt on the runner mirror can crawl for minutes; the build
# step has its own tight limit.
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install cross compiler
run: sudo apt-get update && sudo apt-get install -y g++-arm-linux-gnueabihf
- name: Build (ARM 32-bit)
timeout-minutes: 2
run: arm-linux-gnueabihf-g++ -std=c++11 -Wall -Wextra -Wno-psabi -Werror -c -o /dev/null test/test_32bit_build.cpp
+9 -4
View File
@@ -209,7 +209,11 @@ jobs:
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
# GCC 15 reports a false -Wstringop-overflow in BoringSSL's Keccak
# code, which its -Werror makes fatal. Clang is its primary compiler.
cmake -S . -B build \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
@@ -319,10 +323,9 @@ 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')
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
# Loose, because apt on the runner mirror can crawl for minutes; the test
# step has its own tight limit.
timeout-minutes: 15
env:
DNS_FIXTURE_PORT: "15353"
DNS_FIXTURE_DELAY: "3"
@@ -370,6 +373,8 @@ jobs:
- name: build test binary
run: cd test && make test
- name: run GetAddrInfoAsyncCancelTest
# A normal run takes about 80s.
timeout-minutes: 3
run: |
cd test
ARCH=$(uname -m)
+1 -1
View File
@@ -100,7 +100,7 @@ sse.stop();
```cpp
bool connected = sse.is_connected();
const std::string &id = sse.last_event_id();
std::string id = sse.last_event_id();
```
## Examples
+25 -8
View File
@@ -76,7 +76,10 @@ cpp-httplib supports multiple TLS backends through an abstraction layer:
> A wolfSSL server that verifies client certificates does not resume TLS sessions: wolfSSL shares its session cache between contexts, so a resumed session could carry a certificate checked against another server's client CA.
> [!NOTE]
> **BoringSSL (best-effort):** BoringSSL builds under `CPPHTTPLIB_OPENSSL_SUPPORT` and is exercised by CI against current upstream. Because BoringSSL does not guarantee API stability, support is best-effort — breakage may occasionally land. Two known behavioral differences vs OpenSSL: (1) BoringSSL's public headers require C++14 or later, so consumers must compile accordingly; (2) hostname verification is SAN-only per RFC 6125 §6.4.4 (no CN fallback).
> **BoringSSL (best-effort):** BoringSSL builds under `CPPHTTPLIB_OPENSSL_SUPPORT` and is exercised by CI against current upstream. Because BoringSSL does not guarantee API stability, support is best-effort — breakage may occasionally land. One known difference vs OpenSSL: BoringSSL's public headers require C++14 or later, so consumers must compile accordingly.
> [!NOTE]
> **Hostname verification is SAN-only on every backend** (RFC 9525). A host is matched against the certificate's subjectAltName entries, never its Common Name, so a certificate that names its host only in the CN is rejected. Reissue it with a subjectAltName.
```c++
// Use either OpenSSL, Mbed TLS, or wolfSSL
@@ -102,13 +105,13 @@ cli.enable_server_hostname_verification(false);
```
> [!NOTE]
> The CA certificates are loaded when the client makes its first connection. Changing `set_ca_cert_path()` or `enable_system_ca()` after that has no effect, so make the settings before the first request, or use a new client. `set_ca_cert_store()` and `load_ca_cert_store()` take effect at once. `WebSocketClient` works the same way.
> The CA certificates are loaded when the client makes its first connection. Changing `set_ca_cert_path()` or `enable_system_ca()` after that has no effect, so make the settings before the first request, or use a new client. `set_ca_cert_store()` and `load_ca_cert_store()` take effect at once. `WebSocketClient` works the same way. On `Client` and `SSLClient`, a setter that changes a TLS setting also closes a connection kept alive under the old one, so the next request connects again.
### SSL Error Handling
When SSL operations fail, cpp-httplib provides detailed error information through `ssl_error()` and `ssl_backend_error()`:
- `ssl_error()` - Returns the TLS-level error as a backend-independent `httplib::tls::ErrorCode` value (e.g., `Fatal`, `CertVerifyFailed`), cast to `int`. `CertVerifyFailed` comes from Mbed TLS and wolfSSL. OpenSSL checks the chain and the host name after the handshake, so a failure there leaves this `0`
- `ssl_error()` - Returns the TLS-level error as a backend-independent `httplib::tls::ErrorCode` value (e.g., `Fatal`, `CertVerifyFailed`), cast to `int`. A server certificate that the built-in verification rejects reports `CertVerifyFailed`, or `HostnameMismatch` when only its name does not match
- `ssl_backend_error()` - Returns the backend-specific error code (e.g., `ERR_get_error()` for OpenSSL/wolfSSL, return value for Mbed TLS). With OpenSSL, a certificate verification failure reports the verify result (`X509_V_ERR_*`) here instead. On Windows, when CryptoAPI verifies the chain, a failure there reports the CryptoAPI error code here
A request that fails after the handshake with `Error::Read` or `Error::Write` also carries the TLS error behind it in both values, such as a TLS 1.3 "certificate required" alert from the server. Both values are reset for each request.
@@ -210,6 +213,8 @@ client_pem.key_pem_len = client_key.size();
httplib::SSLClient cli("api.example.com", 443, client_pem);
```
A length of 0 means the field is a NUL-terminated string.
`httplib::ws::WebSocketClient` has the same `PemMemory` constructor for `wss://` connections. See [README-websocket.md](README-websocket.md) for details.
### Peer Certificate Inspection
@@ -238,7 +243,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and
| Platform | Behavior | Disable (compile time) |
| :------- | :------- | :--------------------- |
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
| macOS | Loads system certs from Keychain, honouring their Trust Settings: a certificate set to "Never Trust", or trusted only for another policy such as S/MIME, is not loaded (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend. Revocation checking is best-effort: a revoked server certificate is rejected, while one whose revocation status cannot be determined is accepted. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
On Windows, the CryptoAPI check can also be turned off at runtime. The TLS backend then verifies the chain instead, as on other platforms:
@@ -404,6 +409,8 @@ ret = svr.remove_mount_point("/");
ret = svr.remove_mount_point("/public");
```
A mount point serves a path only when it has no `.`, `..` or empty segment other than a trailing `/`, no segment ending in `.` or a space and, on Windows, no `:` (it names an NTFS stream), so a check on `req.path` in `set_pre_routing_handler()` sees the path of the file that is served. A path ending in `/`, and the mount point itself such as `/public`, serve the `index.html` in that directory. The file system can still give a file other names: another case on a case-insensitive file system, such as the default ones on macOS and Windows, and on Windows an 8.3 short name such as `PRIVAT~1`. Compare paths without regard to case on such a file system, and on Windows keep files that need a guard out of mounted directories.
```cpp
// User defined file extension and MIME type mappings
svr.set_file_extension_and_mimetype_mapping("cc", "text/x-c");
@@ -411,7 +418,7 @@ svr.set_file_extension_and_mimetype_mapping("cpp", "text/x-c");
svr.set_file_extension_and_mimetype_mapping("hh", "text/x-h");
```
Extensions match regardless of case, so `photo.JPG` is served as `image/jpeg`. A user mapping is looked up by the exact extension first, then by the extension in lowercase.
Extensions match regardless of case, so `photo.JPG` is served as `image/jpeg`. The extension of a user mapping can be given in any case, with or without a leading `.`.
The following are built-in mappings:
@@ -518,7 +525,7 @@ svr.set_error_handler([](const auto& req, auto& res) {
```
### Exception handler
The exception handler gets called if a user routing handler throws an error. This covers the pre-routing handler, the file request handler, the pre-request handler, route handlers and content readers. An exception from the post-routing handler, the error handler, the `Expect: 100-continue` handler, a content provider, a logger or a WebSocket handler does not reach it: the connection is dropped, and the error logger receives `Error::UserCallbackException`.
The exception handler gets called if a user routing handler throws an error. This covers the pre-routing handler, the file request handler, the pre-request handler, route handlers, content readers and, on a WebSocket upgrade, the sub-protocol selector. The response is a 500 unless the exception handler sets another status. An exception from the post-routing handler, the error handler, the `Expect: 100-continue` handler, a content provider, a logger or a WebSocket handler does not reach it: the connection is dropped, and the error logger receives `Error::UserCallbackException`.
```cpp
svr.set_exception_handler([](const auto& req, auto& res, std::exception_ptr ep) {
@@ -912,6 +919,14 @@ svr.set_keep_alive_max_count(2); // Default is 100, 0 for no limit
svr.set_keep_alive_timeout(10); // Default is 5
```
### Client address behind a reverse proxy
```cpp
svr.set_trusted_proxies({"10.0.0.2"});
```
A request that comes straight from one of these addresses gets `req.remote_addr` from `X-Forwarded-For`: the rightmost address in it that is not a trusted proxy. A request from anywhere else keeps the address of its connection, whatever `X-Forwarded-For` says. Addresses match as IP addresses, so an IPv4 peer that a server listening on both IPv4 and IPv6 sees as `::ffff:10.0.0.2` matches `10.0.0.2`. Host names and CIDR ranges are not supported.
### Timeout
```c++
@@ -924,7 +939,7 @@ svr.set_read_timeout(std::chrono::seconds(5));
svr.set_keep_alive_timeout(std::chrono::seconds(10));
```
The read timeout bounds the request line and headers as a whole, and each read of a request body.
The read timeout bounds the request line and headers as a whole, and each read of a request body. An error response closes the connection. Any other response to a request whose body was left unread, such as a redirect from a pre-routing handler, keeps the connection only when the rest of the body is at most `CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH` (256KB, `0` for no limit) and arrives within the read timeout, since the server has to read past it first.
### Set maximum payload length for reading a request body
@@ -1445,7 +1460,7 @@ res->status; // 200
A URL with userinfo (`http://user:pass@host/`) is not supported, neither for a `Client` nor in a `Location`; use `set_basic_auth()` for the credentials. A `Client` constructed from a URL it cannot use, such as one with userinfo, a backslash in the authority or an invalid port, is not valid: `is_valid()` returns `false`, and every request fails with `Error::Connection`.
On a redirect to another origin (a different scheme, host or port), `Authorization`, `Cookie` and `Proxy-Authorization` are dropped along with the credentials set with `set_basic_auth()`, `set_bearer_token_auth()` and `set_digest_auth()`, and the client certificate is not presented. Other headers, the default ones included, go along (RFC 9110 §15.4), and so do the timeouts, the payload limit and the TLS verification settings. A `303` that turns a request into a `GET` drops its headers and body; only on the same origin are the default headers sent again.
On a redirect to another origin (a different scheme, host or port), `Authorization`, `Cookie` and `Proxy-Authorization` are dropped along with the credentials set with `set_basic_auth()`, `set_bearer_token_auth()` and `set_digest_auth()`, and the client certificate is not presented. Other headers, the default ones included, go along (RFC 9110 §15.4), and so do the timeouts, the payload limit and the TLS verification settings. The TLS settings pass only from one HTTPS hop to the next: an HTTPS hop that follows a plain HTTP one starts over with the defaults, verifying against the system CAs with no verifier set. A `303` that turns a request into a `GET` drops its headers and body; only on the same origin are the default headers sent again.
Up to `CPPHTTPLIB_REDIRECT_MAX_COUNT` (20 by default) redirects are followed for a request; one more fails it with `Error::ExceedRedirectCount`. Interim `1xx` responses before the final response are skipped, up to `CPPHTTPLIB_INTERIM_RESPONSE_MAX_COUNT` (100 by default); one more fails the request with `Error::Read`.
@@ -1586,6 +1601,8 @@ This holds for every kind of response, including the file-backed ones below.
`Vary: Accept-Encoding` is added only to responses the server encoded itself. A handler that chooses between an encoded and an identity representation by reading `Accept-Encoding` should set the field itself, so that shared caches keep the two apart.
Turn this compression off with `svr.set_compress(false)`. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). A server whose responses mix the two should turn compression off, or keep such secrets out of those responses. Static file compression, below, is set separately.
### Static file compression
Responses served from a file, whether through `set_mount_point()` or `Response::set_file_content()`, are sent as is by default. Turn compression on for them with:
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.62.0"
version = "0.63.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
@@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
});
```
## Turning it off
```cpp
svr.set_compress(false);
```
Handler responses, buffered or streamed, are then sent as they are. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). Static file compression is set separately.
## Static files need to be opted in
Files served as they are, through `set_mount_point()` or `Response::set_file_content()`, are not compressed by default. Turn it on with:
@@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
});
```
## 圧縮を止める
```cpp
svr.set_compress(false);
```
ハンドラのレスポンスは、通常のボディもストリーミングも、そのまま送られます。HTTPSでは、攻撃者が選べる文字列とCSRFトークンなどの秘密が同じレスポンスに載ると、圧縮後の長さから秘密が漏れることがあります(BREACH攻撃)。静的ファイルの圧縮は別に設定します。
## 静的ファイルは明示的に有効にする
`set_mount_point()`や`Response::set_file_content()`でファイルをそのまま返す場合、デフォルトでは圧縮されません。有効にするには次を呼びます。
+683 -468
View File
File diff suppressed because it is too large. Load diff
+8
View File
@@ -153,6 +153,14 @@ if(HTTPLIB_IS_USING_OPENSSL)
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
# cert_dns_cn.pem: CN is the DNS name the tests connect to, with NO
# subjectAltName. A host is named by a SAN only (RFC 9525),
# so verifying it against this cert must fail.
execute_process(
COMMAND ${OPENSSL_COMMAND} req -x509 -key key.pem -sha256 -days 3650 -nodes -subj /CN=localhost -out cert_dns_cn.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
# cert_wildcard_san.pem: a leftmost wildcard next to one that is not
# leftmost. "*.leftmost.example.test" matches a single
# label; the wildcard in "www.*.example.test" must not be
+5
View File
@@ -38,6 +38,11 @@ openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext
# DNS:a.zz is 97.46.122.122, IP:42.46.122.122 is "*.zz".
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=san-types" -addext "subjectAltName=DNS:a.zz,IP:42.46.122.122" -out cert_san_types.pem
# cert_dns_cn.pem: CN is the DNS name the tests connect to, with NO
# subjectAltName. A host is named by a SAN only (RFC 9525), so
# verifying it against this cert must fail.
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=localhost" -out cert_dns_cn.pem
# cert_wildcard_san.pem: a leftmost wildcard next to one that is not leftmost.
# "*.leftmost.example.test" matches a single label; the
# wildcard in "www.*.example.test" must not be honoured.
+11
View File
@@ -146,6 +146,16 @@ cert_san_types_pem = custom_target(
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=san-types', '-addext', 'subjectAltName=DNS:a.zz,IP:42.46.122.122', '-out', '@OUTPUT@']
)
# cert_dns_cn.pem: CN is the DNS name the tests connect to, with NO
# subjectAltName. A host is named by a SAN only (RFC 9525), so verifying it
# against this cert must fail.
cert_dns_cn_pem = custom_target(
'cert_dns_cn_pem',
input: key_pem,
output: 'cert_dns_cn.pem',
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=localhost', '-out', '@OUTPUT@']
)
# cert_wildcard_san.pem: a leftmost wildcard next to one that is not leftmost.
# "*.leftmost.example.test" matches a single label; the wildcard in
# "www.*.example.test" must not be honoured.
@@ -197,6 +207,7 @@ test(
client_encrypted_pbes1_key_pem,
client_encrypted_cert_pem,
cert_ip_cn_pem,
cert_dns_cn_pem,
cert_ipv6_pem,
cert_san_types_pem,
cert_wildcard_san_pem
+1368 -309
View File
File diff suppressed because it is too large. Load diff