Compare commits

...
30 Commits
Author SHA1 Message Date
yhirose 546e821c65 Add server_mount_fuzzer for static file serving (#2666)
Fuzz Server::handle_file_request through a mount point backed by a temporary directory that holds symlinks escaping the mount root, one of them into a sibling directory sharing its name prefix. Abort if a token stored outside the root appears in a response. The seeds are based on those of #2661.

Move FuzzedStream into fuzzed_stream.h so that both server fuzzers share it, and add FuzzableServer::MarkAsRunning(), without which a server that is not listening writes no file.
2026-11-12 21:52:33 -05:00
yhirose 53a7f4964e Add sse_parser_fuzzer for detail::SSEParser (#2665)
Feed the event stream to the parser in chunks and abort if the reported events, ids and retries differ from parsing it in one piece. Also run with a length limit to cover the early-stop path.
2026-11-12 21:11:48 -05:00
Arthur Chan 8c5fd6aa08 OSS-Fuzz: Improve url_parser_fuzzer with more cases (#2664)
Add cases for relative location resolution, dot segment removal, URI encoding and decoding, and filename sanitization.
2026-11-12 20:47:26 -05:00
Arthur Chan a2ebab183d OSS-Fuzz: Add new fuzzer/options targets websocket (#2663)
Add websocket_fuzzer covering frame reading, UTF-8 and close payload validation, and the upgrade response parser. Add websocket_fuzzer.options to raise max_len above the 64 KiB frame length boundary.
2026-11-12 20:47:22 -05:00
yhirose a39e081386 Test SSEParser directly instead of through a server
Move the SSE parsing tests from a fixture that started a server and an
SSEClient for every case to unit tests of detail::SSEParser, and turn
NonDigitRetryFieldIgnored, which slept for 500ms, into one of them. Add
tests that feed each stream split at every position and byte by byte,
and that cover the max event length and an event left incomplete at the
end of the stream.
2026-11-12 19:07:54 -05:00
yhirose 07142256ed Extract SSE event stream parsing into detail::SSEParser
Move the line splitting and event assembly out of
SSEClient::run_event_loop() into a parser that is fed chunks and reports
events, ids and retry values through handlers. The event loop is left
with I/O and reconnection, and the parser can be driven without a
server, which the following test and fuzzer changes rely on.

No behavior change.
2026-11-12 18:27:58 -05:00
userandastunm cc7a30f12f Preserve aliased keys when erasing entries (#2659)
Co-authored-by: astunm <astunm@users.noreply.github.com>
2026-11-11 14:32:09 -05:00
yhirose cdcd2ac6e6 Release v0.66.0 2026-11-10 17:19:39 -05:00
yhirose bd23a7b179 Keep the job queued when a dynamic worker thread cannot be created
ThreadPool::enqueue constructed the dynamic thread without handling
std::system_error. When thread creation failed (e.g. EAGAIN under a
pids limit), the exception escaped the accept loop and destroying the
pool's joinable threads called std::terminate. Treat the failure as
"no spare capacity" and leave the job for an existing worker.
2026-11-10 16:54:53 -05:00
metsw24-max 6d8dec3c3e Keep a redirect on the unix socket in ClientImpl::redirect (#2658)
A client using set_address_family(AF_UNIX) took the host of an absolute Location as a socket path, so a redirect to http://localhost/next failed and one to http://other.sock/next went to ./other.sock. Every hop now stays on the client's socket, as with curl's --unix-socket.

The same host is still decided from the Location, as curl does: only localhost (the Host this client sends) counts as this client, so a redirect to any other host goes through the cross-host client and drops Authorization and Cookie. That client reaches the original socket through addr_map_, and a host the caller mapped with set_direct_hostname_addr_map keeps its mapping. An https Location runs TLS over the socket with the Location's host.

The mapped-target lookup shared with make_connect_request_target is factored into detail::addr_map_target.
2026-11-07 10:22:00 -05:00
yhirose 7e5d0683ac Release v0.65.0 2026-11-05 07:39:47 -05:00
yhirose 6df971434e Do not carry Request::params over to a redirect's Location
A request sent with send(Request) and params got those params appended
to the Location of a redirect that had no query, on the same origin or
another one. The Location alone names the next target, as it already
did for Get/Post(path, params).
2026-11-05 07:04:31 -05:00
Eminy 39c98d4671 Ignore a Range header with a unit other than bytes (#2657)
RFC 9110 14.2 says an origin server MUST ignore a Range header field
with a range unit it does not understand. parse_range_header() accepts
only bytes=, so a request such as "Range: items=1-2" got a 416 instead
of the full response. The unit is now checked first, and only a bytes
range is parsed and answered with a 416 when it is invalid.
2026-11-04 22:44:22 -05:00
Wei-Lun Liandyhirose a1ba73a256 Cap a send() or wolfSSL_read() length at INT_MAX (#2655)
* Cap a send() or wolfSSL_read() length at INT_MAX

On 64-bit Windows SocketStream::write() handed size to send(), whose
length is an int, so a single write of 4 GiB or more went out as its low
32 bits: a 4 GiB + 1 byte write sent one byte, and write_data() then
retried the remaining 4 GiB as zero-byte sends forever. The INT_MAX cap
that read() has was limited to 32-bit Windows in df20c27 (#1294), when
the body was still written with one unchecked write(); write_data() now
sends the rest, so the cap applies to every Windows build again.

The wolfSSL tls::read() had the same cast, so a read into a buffer over
INT_MAX failed. Cap it as the OpenSSL backend has done since f80864c.

* Drop the 4 GiB write test and shorten comments

The Windows-only test allocated 4 GiB and took about 5 s, which is too
heavy for CI.

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-11-04 18:06:30 -05:00
metsw24-maxandyhirose 453b1b8613 keep a relative Location on its host in resolve_relative_location (#2654)
* keep a relative Location on its host in resolve_relative_location

* Shorten the comment on on_this_host in resolve_relative_location

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-11-02 13:12:31 -05:00
Yoshiki Matsuda a18895f42e Let ClientImpl::stop() interrupt a request waiting on Windows (#2653)
stop() shuts the socket down to end a request in flight, but on Windows
that does not wake a read waiting in WSAPoll (#1396). The request then
waited until the server answered, the read timeout passed, or the OS
dropped the connection.

stop() now also sets a flag, and send_() gives the request's stream an
interrupt check that reads it, as SSEClient::stop() does since 801bf2e.
The wait for data then ends within 100 ms on every platform. A request
started after stop() clears the flag. A redirect or a digest retry sent
inside the request keeps it.

select_read_interruptible() now rounds its last wait up to a whole
millisecond. poll() takes milliseconds, and the wait ended up to 1 ms
before the deadline, which MaxTimeoutTest caught once every client
request waited through this function.
2026-10-30 13:40:50 -04:00
yhirose 24e58ce5b2 Release v0.64.0 2026-10-29 17:08:34 -04:00
Kim, Hyuk a0ab09fc0a Add set_basic_auth and set_bearer_token_auth to WebSocketClient (#2652)
ClientImpl and SSLClient let callers set origin credentials with
set_basic_auth()/set_bearer_token_auth(), but WebSocketClient could only
take a fixed Authorization header through its constructor. Add both
setters: each connect() adds the Authorization header to the upgrade
request from the current credentials (Basic over Bearer), unless the
constructor headers already carry one, and never to the proxy's CONNECT
request.
2026-10-28 16:15:17 -04:00
Kim, Hyuk 290c4c0bbe Add set_tunnel_hostname_addr_map() and deprecate set_hostname_addr_map() (#2647)
Through a CONNECT proxy it is the proxy that connects to the origin, and the CONNECT target was always the original hostname, so there was no way to send the tunnel to another address. set_tunnel_hostname_addr_map() sets that target on SSLClient, Client and WebSocketClient. The CONNECT request's Host follows it, including the Digest retry, while the Host header, the SNI and the certificate check inside the tunnel keep the original hostname. The map is also copied to a redirect client.

set_hostname_addr_map() is deprecated in favor of set_direct_hostname_addr_map(), which behaves the same and names the connection it applies to. The two maps are independent and correspond to curl's --resolve and --connect-to; neither applies to the proxy's own address.
2026-10-27 21:09:25 -04:00
metsw24-max 25ba40f6fe Reject an EOF without close_notify in the Mbed TLS and wolfSSL read (#2649)
Mbed TLS and wolfSSL read a TLS EOF without close_notify as a clean end, so a response delimited by the connection close could be cut short and still come back complete. Both reads now end cleanly only on a close_notify, as on OpenSSL.
2026-10-27 18:31:55 -04:00
metsw24-max 578e4815b7 Treat ':' as a path separator in sanitize_filename on Windows (#2651)
On Windows a ':' ends a drive prefix or starts an NTFS stream name, so a filename such as "C:.." or "a.txt:stream" survived sanitize_filename and could replace the upload directory or write to an alternate data stream. Cut at ':' on Windows as well.
2026-10-27 18:16:21 -04:00
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
32 changed files with 2586 additions and 623 deletions

No files matched your search

+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
+22 -1
View File
@@ -200,6 +200,10 @@ void close(CloseStatus status = CloseStatus::Normal,
const std::string &reason = "");
bool is_open() const;
// Authentication - the same setters Client has (Basic wins over Bearer).
void set_basic_auth(const std::string &username, const std::string &password);
void set_bearer_token_auth(const std::string &token);
// Proxy - the same setters Client has. connect() opens a CONNECT tunnel
// through the proxy for ws:// as well as wss:// (RFC 6455 4.1); the proxy
// credentials are sent only on the CONNECT request, never through the tunnel.
@@ -233,7 +237,8 @@ void set_address_family(int family);
void set_ipv6_v6only(bool on);
void set_socket_options(SocketOptions socket_options);
void set_interface(const std::string &intf);
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
// SSL configuration (wss:// only, SSL builds only)
void set_ca_cert_path(const std::string &ca_cert_file_path,
@@ -403,6 +408,22 @@ svr.set_pre_request_handler([](const httplib::Request &req, httplib::Response &r
The server accepts a handshake from any `Origin`. A browser sends its cookies with a handshake another site starts, so if cookies authenticate the connection, also check `req.get_header_value("Origin")` against your own origins in one of these handlers (cross-site WebSocket hijacking).
### Authentication
```cpp
httplib::ws::WebSocketClient ws("wss://example.com/ws");
ws.set_bearer_token_auth("token123"); // Authorization: Bearer token123
// or
ws.set_basic_auth("user", "pass"); // Authorization: Basic dXNlcjpwYXNz
if (ws.connect()) { /* ... */ }
```
The credentials are read on every `connect()`, so a new token takes effect on
the next reconnect. An `Authorization` header passed to the constructor wins
over them. Through a proxy they are sent only inside the tunnel, never on
`CONNECT`.
### Custom Headers and Timeouts
```cpp
+41 -12
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
@@ -240,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:
@@ -406,7 +409,7 @@ 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 `/`, and no segment ending in `.` or a space, 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.
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
@@ -739,7 +742,7 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
auto safe = httplib::sanitize_filename(file.filename);
```
This function keeps only the part after the last `/` or `\`, removes null bytes, and trims leading and trailing spaces and tabs. It returns an empty string if what is left is empty, `.` or `..`.
This function keeps only the part after the last `/` or `\` (or, on Windows, `:`, which starts a drive prefix such as `C:` or an NTFS stream name), removes null bytes, and trims leading and trailing spaces and tabs. It returns an empty string if what is left is empty, `.` or `..`.
> [!NOTE]
> `name` and `filename` are read as quoted-strings (RFC 9110 §5.6.4), where `\"` and `\\` stand for `"` and `\`. Browsers send a backslash as it is, so from a browser, a value with `\\` in it arrives with a single `\`, and one that ends in `\` is not read correctly.
@@ -1474,17 +1477,17 @@ The same method is available on `httplib::ws::WebSocketClient`.
### Override the connection target for a hostname
`set_hostname_addr_map` redirects where the socket connects, without changing
the identity of the request. The hostname the client was constructed with keeps
supplying the `Host` header, the SNI, and the name that the server certificate
is verified against, so this is a connection-level override only, not a way to
talk to a different origin.
`set_direct_hostname_addr_map` redirects where the socket connects, without
changing the identity of the request. The hostname the client was constructed
with keeps supplying the `Host` header, the SNI, and the name that the server
certificate is verified against, so this is a connection-level override only,
not a way to talk to a different origin.
```cpp
httplib::Client cli("https://example.com");
// Connect to this IP address instead of resolving "example.com"
cli.set_hostname_addr_map({{"example.com", "192.168.1.10"}});
cli.set_direct_hostname_addr_map({{"example.com", "192.168.1.10"}});
```
A mapped value may be an IP literal or another hostname. An IP literal is used
@@ -1492,13 +1495,37 @@ as-is; anything else is resolved as a name, so a host that is only reachable
under a different name works too:
```cpp
cli.set_hostname_addr_map({{"example.com", "internal.example.lan"}});
cli.set_direct_hostname_addr_map({{"example.com", "internal.example.lan"}});
```
An empty value is ignored, leaving the original hostname as the connection
target.
The same method is available on `httplib::ws::WebSocketClient`.
The direct map applies only when the client connects to the origin itself.
Through a proxy, use `set_tunnel_hostname_addr_map`: its value becomes the
`CONNECT` target (and that request's `Host`) that the proxy is asked to connect
to (HTTPS and WebSocket). The `Host` header, the SNI and the certificate check
inside the tunnel keep the original hostname:
```cpp
httplib::Client cli("https://sni.example");
cli.set_tunnel_hostname_addr_map({{"sni.example", "server.example"}});
cli.set_proxy("proxy.example", 3128);
// CONNECT server.example:443, SNI sni.example
```
The two maps are independent; to use the same target on both paths, set both.
They correspond to curl's `--resolve` and `--connect-to`.
Neither map applies to the proxy's own address ("direct" means not through a
proxy, as in a PAC file's `DIRECT`). To connect to the proxy at a fixed
address, pass an IP literal to `set_proxy()`.
`set_hostname_addr_map` is deprecated and works the same as
`set_direct_hostname_addr_map`.
These methods are also available on `httplib::ws::WebSocketClient`.
### Automatic Path Encoding
@@ -1689,6 +1716,8 @@ cli.set_address_family(AF_UNIX);
This library automatically sets the Host header to "localhost" for Unix socket connections, similar to curl's behavior.
With `set_follow_location(true)`, a redirect is followed over the same socket, whatever host the `Location` names, as curl's `--unix-socket` does. A host mapped with `set_direct_hostname_addr_map` goes to its mapped socket instead. Only a `Location` naming `localhost` counts as the same host; any other host is treated as cross-host, so `Authorization` and `Cookie` are not sent to it.
URI Encoding/Decoding Utilities
-------------------------------
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.63.0"
version = "0.66.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+1 -1
View File
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // The port is unused, but must not be 0
// Client
httplib::Client cli("http://localhost");
cli.set_address_family(AF_UNIX);
cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
auto res = cli.Get("/");
```
+1 -1
View File
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // ポート番号は使われない(0
// クライアント
httplib::Client cli("http://localhost");
cli.set_address_family(AF_UNIX);
cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
auto res = cli.Get("/");
```
+526 -253
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
+13 -2
View File
@@ -13,14 +13,15 @@ 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 client_fuzzer multipart_parser_fuzzer
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer multipart_parser_fuzzer websocket_fuzzer sse_parser_fuzzer server_mount_fuzzer
# Runs all the tests and also fuzz tests against seed corpus.
all : $(FUZZERS)
./server_fuzzer corpus/*
./server_mount_fuzzer corpus_mount/*
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
server_fuzzer : server_fuzzer.cc ../../httplib.h
server_fuzzer : server_fuzzer.cc fuzzed_stream.h ../../httplib.h
# $(CXX) $(CXXFLAGS) -o $@ $< -Wl,-Bstatic $(OPENSSL_SUPPORT) -Wl,-Bdynamic -ldl $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
zip -q -r server_fuzzer_seed_corpus.zip corpus
@@ -38,5 +39,15 @@ url_parser_fuzzer : url_parser_fuzzer.cc ../../httplib.h
multipart_parser_fuzzer : multipart_parser_fuzzer.cc ../../httplib.h
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
websocket_fuzzer : websocket_fuzzer.cc ../../httplib.h
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
sse_parser_fuzzer : sse_parser_fuzzer.cc ../../httplib.h
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
server_mount_fuzzer : server_mount_fuzzer.cc fuzzed_stream.h ../../httplib.h
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
zip -q -r server_mount_fuzzer_seed_corpus.zip corpus_mount
clean:
rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip
+3
View File
@@ -0,0 +1,3 @@
HEAD /mnt/a.txt HTTP/1.1
Host: x
+4
View File
@@ -0,0 +1,4 @@
GET /mnt/a.txt HTTP/1.1
Host: x
If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT
+3
View File
@@ -0,0 +1,3 @@
GET /mnt/index.html HTTP/1.1
Host: x
+4
View File
@@ -0,0 +1,4 @@
GET /mnt/index.html HTTP/1.1
Host: x
If-None-Match: W/"bogusetag"
+3
View File
@@ -0,0 +1,3 @@
GET /mnt/link-in.txt HTTP/1.1
Host: x
+3
View File
@@ -0,0 +1,3 @@
GET /mnt/link-out.txt HTTP/1.1
Host: x
@@ -0,0 +1,3 @@
GET /mnt/link-sibling.txt HTTP/1.1
Host: x
+3
View File
@@ -0,0 +1,3 @@
GET /mnt/missing.txt HTTP/1.1
Host: x
+4
View File
@@ -0,0 +1,4 @@
GET /mnt/a.txt HTTP/1.1
Host: x
Range: bytes=0-3
+3
View File
@@ -0,0 +1,3 @@
GET /mnt/sub/ HTTP/1.1
Host: x
@@ -0,0 +1,3 @@
GET /mnt/../../../../etc/passwd HTTP/1.1
Host: x
@@ -0,0 +1,3 @@
GET /mnt/%2e%2e/secret.txt HTTP/1.1
Host: x
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include <cstdint>
#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;
// Like SocketStream, pull in more than was asked for, which the line
// reader then takes through buffered_data(), so both of its paths run.
buffered_end_ =
(std::max)(buffered_end_, (std::min)(size_, read_pos_ + 4096));
return static_cast<ssize_t>(size);
}
const char *buffered_data(size_t &size) const override {
size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0;
return size ? reinterpret_cast<const char *>(data_ + read_pos_) : nullptr;
}
void consume_buffered(size_t size) override { read_pos_ += size; }
ssize_t write(const char *ptr, size_t size) override {
response_.append(ptr, size);
return static_cast<int>(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; };
const std::string &response() const { return response_; }
private:
const uint8_t *data_;
size_t size_;
size_t read_pos_;
size_t buffered_end_ = 0;
std::string response_;
};
class FuzzableServer : public httplib::Server {
public:
// A server that is not listening counts as shutting down, and then writes
// no file. The socket is never used.
void MarkAsRunning() { svr_sock_ = 0; }
void ProcessFuzzedRequest(FuzzedStream &stream) {
bool connection_close = false;
process_request(stream,
/*remote_addr=*/"",
/*remote_port =*/0,
/*local_addr=*/"",
/*local_port =*/0,
/*last_connection=*/false, connection_close, nullptr);
}
};
+1 -76
View File
@@ -1,79 +1,4 @@
#include <cstdint>
#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;
// Like SocketStream, pull in more than was asked for, which the line
// reader then takes through buffered_data(), so both of its paths run.
buffered_end_ =
(std::max)(buffered_end_, (std::min)(size_, read_pos_ + 4096));
return static_cast<ssize_t>(size);
}
const char *buffered_data(size_t &size) const override {
size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0;
return size ? reinterpret_cast<const char *>(data_ + read_pos_) : nullptr;
}
void consume_buffered(size_t size) override { read_pos_ += size; }
ssize_t write(const char *ptr, size_t size) override {
response_.append(ptr, size);
return static_cast<int>(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_;
size_t buffered_end_ = 0;
std::string response_;
};
class FuzzableServer : public httplib::Server {
public:
void ProcessFuzzedRequest(FuzzedStream &stream) {
bool connection_close = false;
process_request(stream,
/*remote_addr=*/"",
/*remote_port =*/0,
/*local_addr=*/"",
/*local_port =*/0,
/*last_connection=*/false, connection_close, nullptr);
}
};
#include "fuzzed_stream.h"
static FuzzableServer g_server;
+117
View File
@@ -0,0 +1,117 @@
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <random>
#include <string>
#include <vector>
#include <sys/stat.h>
#include <unistd.h>
#include <utime.h>
#include "fuzzed_stream.h"
// Fixture layout, with <token> unknown to the fuzzer:
// root/mnt/{index.html,a.txt,data.bin,sub/index.html} served at /mnt
// root/mnt/link-in.txt -> a.txt (stays inside, may be served)
// root/mnt/link-out.txt -> ../outside.txt (escapes, must be refused)
// root/mnt/link-sibling.txt -> ../mnt_secret/secret.txt (shares the "mnt"
// prefix, must be refused)
// The response must never contain <token>.
namespace {
FuzzableServer g_server;
std::string g_root;
std::string g_token;
std::vector<std::string> g_paths; // In creation order
void check(bool ok, const char *what) {
if (!ok) {
fprintf(stderr, "fixture setup failed: %s\n", what);
abort();
}
}
void make_dir(const std::string &path) {
check(mkdir(path.c_str(), 0755) == 0, path.c_str());
g_paths.push_back(path);
}
void make_file(const std::string &path, const std::string &content) {
{
std::ofstream out(path, std::ios::binary);
out << content;
check(out.good(), path.c_str());
}
// Fixed, so that conditional requests behave the same in every run
utimbuf times = {1700000000, 1700000000};
check(utime(path.c_str(), &times) == 0, path.c_str());
g_paths.push_back(path);
}
void make_link(const std::string &target, const std::string &path) {
check(symlink(target.c_str(), path.c_str()) == 0, path.c_str());
g_paths.push_back(path);
}
std::string serve(const uint8_t *data, size_t size) {
FuzzedStream stream{data, size};
g_server.ProcessFuzzedRequest(stream);
return stream.response();
}
void check_served(const char *path) {
auto req = std::string("GET ") + path + " HTTP/1.1\r\nHost: x\r\n\r\n";
auto res = serve(reinterpret_cast<const uint8_t *>(req.data()), req.size());
// Without this, a fuzzer that serves nothing would pass its check forever
check(res.find("abcdef") != std::string::npos, path);
}
void remove_fixture() {
for (auto it = g_paths.rbegin(); it != g_paths.rend(); ++it) {
if (unlink(it->c_str()) != 0) { rmdir(it->c_str()); }
}
rmdir(g_root.c_str());
}
} // namespace
extern "C" int LLVMFuzzerInitialize(int * /*argc*/, char *** /*argv*/) {
std::random_device rd;
char token[33];
snprintf(token, sizeof(token), "%08x%08x%08x%08x", rd(), rd(), rd(), rd());
g_token = token;
auto tmpdir = getenv("TMPDIR");
auto tmpl = std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") +
"/httplib-mount-XXXXXX";
check(mkdtemp(&tmpl[0]) != nullptr, tmpl.c_str());
g_root = tmpl;
atexit(remove_fixture);
auto mnt = g_root + "/mnt";
make_dir(mnt);
make_dir(mnt + "/sub");
make_dir(g_root + "/mnt_secret");
make_file(mnt + "/index.html", "<html>index</html>");
make_file(mnt + "/a.txt", "abcdef");
make_file(mnt + "/data.bin", std::string("\x01\x02\x03\x04\x05\x06", 6));
make_file(mnt + "/sub/index.html", "<html>sub</html>");
make_file(g_root + "/outside.txt", g_token);
make_file(g_root + "/mnt_secret/secret.txt", g_token);
make_link("a.txt", mnt + "/link-in.txt");
make_link("../outside.txt", mnt + "/link-out.txt");
make_link("../mnt_secret/secret.txt", mnt + "/link-sibling.txt");
g_server.MarkAsRunning();
check(g_server.set_mount_point("/mnt", mnt), "set_mount_point");
check_served("/mnt/a.txt");
check_served("/mnt/link-in.txt");
return 0;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (serve(data, size).find(g_token) != std::string::npos) { abort(); }
return 0;
}
+24
View File
@@ -0,0 +1,24 @@
"GET"
"HEAD"
"HTTP/1.1"
"/mnt/"
"/mnt/index.html"
"/mnt/a.txt"
"/mnt/data.bin"
"/mnt/sub/"
"/mnt/link-in.txt"
"/mnt/link-out.txt"
"/mnt/link-sibling.txt"
".."
"%2e%2e"
"%2f"
"//"
"Range: bytes=0-"
"Range: bytes=0-3,5-"
"If-None-Match: "
"W/"
"If-Modified-Since: "
"If-Range: "
"Accept-Encoding: gzip"
+49
View File
@@ -0,0 +1,49 @@
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <httplib.h>
namespace {
// Feeds the stream in chunks and records every handler call in order
std::string parse_in_chunks(const std::string &stream, size_t chunk,
size_t max_event_length) {
std::string trace;
httplib::detail::SSEParser parser(
[&](const httplib::sse::SSEMessage &msg) {
trace += "event[" + msg.event + "|" + msg.data + "|" + msg.id + "]";
},
[&](const std::string &id) { trace += "id[" + id + "]"; },
[&](int retry_ms) { trace += "retry[" + std::to_string(retry_ms) + "]"; },
max_event_length);
for (size_t off = 0; off < stream.size(); off += chunk) {
if (!parser.parse(stream.data() + off,
std::min(chunk, stream.size() - off))) {
break;
}
}
return trace;
}
} // namespace
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size < 2 || size > 65536) { return 0; }
// First byte selects the chunk size, the rest is the event stream
auto chunk = static_cast<size_t>(data[0]) + 1;
std::string stream(reinterpret_cast<const char *>(data + 1), size - 1);
// Without a length limit, chunking must not change what is reported
auto whole = parse_in_chunks(stream, stream.size(), 0);
auto chunked = parse_in_chunks(stream, chunk, 0);
if (whole != chunked) { std::abort(); }
// With a limit, only check that the parser stops cleanly
parse_in_chunks(stream, chunk, 64);
return 0;
}
+43 -1
View File
@@ -13,7 +13,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
size_t payload_size = size - 1;
std::string input(payload, payload_size);
switch (selector % 6) {
switch (selector % 13) {
case 0: {
// parse_query_text
httplib::Params params;
@@ -46,6 +46,48 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
httplib::detail::is_valid_path(input);
break;
}
case 6: {
// parse_url
httplib::detail::UrlComponents uc;
httplib::detail::parse_url(input, uc);
break;
}
case 7: {
// remove_dot_segments
httplib::detail::remove_dot_segments(input);
break;
}
case 8: {
// resolve_relative_location, with the input as "<location>\n<base>"
auto nl = input.find('\n');
auto location = input.substr(0, nl);
auto base = nl == std::string::npos ? std::string("/index.html")
: input.substr(nl + 1);
httplib::detail::resolve_relative_location(location, base);
break;
}
case 9: {
// encode_path
httplib::detail::encode_path(input);
break;
}
case 10: {
// encode_uri_component, encode_uri
httplib::encode_uri_component(input);
httplib::encode_uri(input);
break;
}
case 11: {
// decode_uri_component, decode_uri
httplib::decode_uri_component(input);
httplib::decode_uri(input);
break;
}
case 12: {
// sanitize_filename
httplib::sanitize_filename(input);
break;
}
}
return 0;
+54
View File
@@ -0,0 +1,54 @@
#include <cstdint>
#include <string>
#include <httplib.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size < 2) { return 0; }
auto selector = data[0];
std::string input(reinterpret_cast<const char *>(data + 1), size - 1);
switch (selector % 4) {
case 0: {
// read_websocket_frame. WebSocket::read passes the macro, close() passes
// 125, and 0 means no limit
static const size_t max_lens[] = {0, 125,
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH};
auto expect_masked = (selector / 4) % 2 != 0;
auto max_len = max_lens[(selector / 8) % 3];
httplib::detail::BufferStream strm;
strm.write(input.data(), input.size());
auto opcode = httplib::ws::Opcode::Text;
std::string payload;
auto fin = false;
httplib::ws::impl::read_websocket_frame(strm, opcode, payload, fin,
expect_masked, max_len);
break;
}
case 1: {
// is_valid_utf8
httplib::ws::impl::is_valid_utf8(input);
break;
}
case 2: {
// is_valid_close_payload
httplib::ws::impl::is_valid_close_payload(input);
break;
}
case 3: {
// read_websocket_upgrade_response, with a fixed key since
// perform_websocket_handshake draws a random one
static const auto expected_accept =
httplib::detail::websocket_accept_key("dGhlIHNhbXBsZSBub25jZQ==");
httplib::detail::BufferStream strm;
strm.write(input.data(), input.size());
httplib::detail::WebSocketUpgradeResponse upgrade;
httplib::detail::read_websocket_upgrade_response(strm, expected_accept,
"chat, stream", upgrade);
break;
}
}
return 0;
}
+2
View File
@@ -0,0 +1,2 @@
[libfuzzer]
max_len = 70000
+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
+1543 -274
View File
File diff suppressed because it is too large. Load diff