mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-11-13 17:00:40 +00:00
Compare commits
30
Commits
a210b43e11
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
546e821c65 | ||
|
|
53a7f4964e | ||
|
|
8c5fd6aa08 | ||
|
|
a2ebab183d | ||
|
|
a39e081386 | ||
|
|
07142256ed | ||
|
|
cc7a30f12f | ||
|
|
cdcd2ac6e6 | ||
|
|
bd23a7b179 | ||
|
|
6d8dec3c3e | ||
|
|
7e5d0683ac | ||
|
|
6df971434e | ||
|
|
39c98d4671 | ||
|
|
a1ba73a256 | ||
|
|
453b1b8613 | ||
|
|
a18895f42e | ||
|
|
24e58ce5b2 | ||
|
|
a0ab09fc0a | ||
|
|
290c4c0bbe | ||
|
|
25ba40f6fe | ||
|
|
578e4815b7 | ||
|
|
252dbaa505 | ||
|
|
8710fbd25d | ||
|
|
2f8b9f6f57 | ||
|
|
391de32115 | ||
|
|
ba32b468a4 | ||
|
|
76f84a5bfd | ||
|
|
d23d1b6029 | ||
|
|
7d349cbeed | ||
|
|
5ceef83c54 |
No files matched your search
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
-------------------------------
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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("/");
|
||||
```
|
||||
|
||||
@@ -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("/");
|
||||
```
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
HEAD /mnt/a.txt HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
GET /mnt/a.txt HTTP/1.1
|
||||
Host: x
|
||||
If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/index.html HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
GET /mnt/index.html HTTP/1.1
|
||||
Host: x
|
||||
If-None-Match: W/"bogusetag"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/link-in.txt HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/missing.txt HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
GET /mnt/a.txt HTTP/1.1
|
||||
Host: x
|
||||
Range: bytes=0-3
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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(), ×) == 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;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[libfuzzer]
|
||||
max_len = 70000
|
||||
@@ -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.
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user