Compare commits

..
9 Commits
Author SHA1 Message Date
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
5 changed files with 469 additions and 106 deletions

No files matched your search

+12 -2
View File
@@ -102,7 +102,7 @@ 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
@@ -916,6 +916,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++
@@ -1449,7 +1457,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`.
@@ -1590,6 +1598,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:
@@ -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()`でファイルをそのまま返す場合、デフォルトでは圧縮されません。有効にするには次を呼びます。
+137 -69
View File
@@ -2357,6 +2357,8 @@ public:
Server &set_payload_max_length(size_t length);
Server &set_compress(bool on);
Server &set_static_file_compression(bool on);
Server &set_static_file_compression_min_length(size_t length);
Server &set_static_file_compression_max_length(size_t length);
@@ -2431,6 +2433,7 @@ protected:
time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
bool compress_ = true;
bool static_file_compression_ = false;
size_t static_file_compression_min_length_ =
CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH;
@@ -2997,6 +3000,15 @@ protected:
void close_socket(Socket &socket);
void disconnect(bool gracefully);
// Under socket_mutex_: whether the connection kept alive can take another
// request. One the peer has closed, or one set up under TLS settings changed
// since, is closed instead.
bool socket_reusable();
// TLS settings apply as a connection is set up, so a change makes the
// connection kept alive unfit for the next request.
void tls_settings_changed() { tls_settings_version_++; }
std::string make_request_target(const Request &req) const;
bool process_request(Stream &strm, Request &req, Response &res,
bool close_connection, Error &error);
@@ -3023,6 +3035,9 @@ protected:
size_t socket_requests_in_flight_ = 0;
std::thread::id socket_requests_are_from_thread_ = std::thread::id();
bool socket_should_be_closed_when_request_is_done_ = false;
size_t socket_tls_settings_version_ = 0;
std::atomic<size_t> tls_settings_version_{0};
// Hostname to connection target map. The value is an IP literal or another
// hostname; only the connection target changes, never the identity.
@@ -4019,6 +4034,7 @@ struct ChunkedDecoder {
char line_buf[64];
size_t last_chunk_total = 0;
size_t last_chunk_offset = 0;
size_t overhead = 0;
explicit ChunkedDecoder(Stream &s);
@@ -8418,6 +8434,18 @@ inline void set_header(Headers &headers, const std::string &key,
if (fields::is_field_valid(key, val)) { headers.emplace(key, val); }
}
// Drops the fields set_header() would refuse, from headers an application can
// fill directly: CR/LF in one would split the message.
inline void erase_invalid_fields(Headers &headers) {
for (auto it = headers.begin(); it != headers.end();) {
if (fields::is_field_valid(it->first, it->second)) {
++it;
} else {
it = headers.erase(it);
}
}
}
inline bool read_headers(Stream &strm, Headers &headers) {
const auto bufsiz = 2048;
char buf[bufsiz];
@@ -8895,13 +8923,6 @@ inline ssize_t write_response_line(Stream &strm, int status) {
inline ssize_t write_headers(Stream &strm, const Headers &headers) {
ssize_t write_len = 0;
for (const auto &x : headers) {
// Skip fields with invalid names or values to prevent response splitting
// via CR/LF injection, matching set_header(). The client validates request
// headers up front in check_and_write_headers, but the server passes
// res.headers straight to this writer, and res.headers is a public field
// an application can populate directly with request-derived values.
if (!fields::is_field_valid(x.first, x.second)) { continue; }
std::string s;
s = x.first;
s += ": ";
@@ -10649,6 +10670,32 @@ inline bool is_ip_address(const std::string &host) {
inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
}
// One spelling for an IPv6 address, and the IPv4 one for an IPv4-mapped IPv6
// address, which is how a dual-stack socket reports an IPv4 peer. Anything
// else, including an address with a zone ID, is returned as it is.
inline std::string canonical_ip(const std::string &addr) {
struct in6_addr addr6;
if (addr.find('%') != std::string::npos ||
inet_pton(AF_INET6, addr.c_str(), &addr6) != 1) {
return addr;
}
char buf[INET6_ADDRSTRLEN];
if (IN6_IS_ADDR_V4MAPPED(&addr6)) {
return inet_ntop(AF_INET, &addr6.s6_addr[12], buf, sizeof(buf)) ? buf
: addr;
}
return inet_ntop(AF_INET6, &addr6, buf, sizeof(buf)) ? buf : addr;
}
// `trusted_proxies` holds canonical_ip() spellings.
inline bool is_trusted_proxy(const std::string &addr,
const std::vector<std::string> &trusted_proxies) {
if (trusted_proxies.empty()) { return false; }
return std::find(trusted_proxies.begin(), trusted_proxies.end(),
canonical_ip(addr)) != trusted_proxies.end();
}
// Resolve where a client should connect for `host`, honoring a user-supplied
// hostname-to-address map. `host` itself is never rewritten, so it keeps
// supplying the Host header and SNI; only the connection target changes.
@@ -13802,7 +13849,11 @@ inline Server &Server::set_header_writer(
inline Server &
Server::set_trusted_proxies(const std::vector<std::string> &proxies) {
trusted_proxies_ = proxies;
trusted_proxies_.clear();
trusted_proxies_.reserve(proxies.size());
for (const auto &proxy : proxies) {
trusted_proxies_.push_back(detail::canonical_ip(proxy));
}
return *this;
}
@@ -13848,6 +13899,11 @@ inline Server &Server::set_payload_max_length(size_t length) {
return *this;
}
inline Server &Server::set_compress(bool on) {
compress_ = on;
return *this;
}
inline Server &Server::set_static_file_compression(bool on) {
static_file_compression_ = on;
return *this;
@@ -14003,6 +14059,10 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
need_apply_ranges = true;
}
// Before anything below reads res.headers, which the handlers may have
// filled directly
detail::erase_invalid_fields(res.headers);
if (!detail::body_allowed_for_status(res.status)) {
detail::clear_content(res);
}
@@ -14046,6 +14106,7 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
if (post_routing_handler_) {
post_routing_handler_(req, res);
detail::erase_invalid_fields(res.headers);
// Again, for a body or framing headers the handler changed.
detail::set_framing_headers(req, res, boundary, content_type);
}
@@ -14871,7 +14932,8 @@ inline void Server::apply_ranges(const Request &req, Response &res,
"multipart/byteranges; boundary=" + boundary);
}
auto type = detail::encoding_type(req, res);
auto type =
compress_ ? detail::encoding_type(req, res) : detail::EncodingType::None;
if (res.body.empty()) {
if (res.content_length_ > 0 && req.ranges.size() == 1 &&
@@ -14978,12 +15040,7 @@ get_client_ip(const std::string &x_forwarded_for,
// returned as the client.
for (size_t i = ip_list.size(); i-- > 0;) {
const auto &ip = ip_list[i];
auto is_trusted_proxy =
std::any_of(trusted_proxies.begin(), trusted_proxies.end(),
[&](const std::string &proxy) { return ip == proxy; });
if (!is_trusted_proxy) { return ip; }
if (!detail::is_trusted_proxy(ip, trusted_proxies)) { return ip; }
}
// Every hop was a trusted proxy; fall back to the first entry.
@@ -15115,11 +15172,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
// Only honor X-Forwarded-For if the peer on the actual TCP connection is
// itself a trusted proxy. Otherwise any direct client could spoof
// remote_addr simply by sending an arbitrary X-Forwarded-For header.
auto is_trusted_peer = std::any_of(
trusted_proxies_.begin(), trusted_proxies_.end(),
[&](const std::string &proxy) { return proxy == remote_addr; });
if (is_trusted_peer && req.has_header("X-Forwarded-For")) {
if (detail::is_trusted_proxy(remote_addr, trusted_proxies_) &&
req.has_header("X-Forwarded-For")) {
// Some proxies append the address they observed as a separate
// X-Forwarded-For field line instead of extending the one the client sent
// (e.g. HAProxy's "option forwardfor"), so the whole combined value has to
@@ -15148,7 +15202,9 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
}
}
if (req.has_header("Range")) {
// RFC 9110 14.2: range handling is defined only for GET (and so for HEAD)
if ((req.method == "GET" || req.method == "HEAD") &&
req.has_header("Range")) {
const auto &range_header_value = req.get_header_value("Range");
if (!detail::parse_range_header(range_header_value, req.ranges)) {
connection_closed = true;
@@ -15526,6 +15582,7 @@ inline bool ClientImpl::create_and_connect_socket(Socket &socket,
}
inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
socket_tls_settings_version_ = tls_settings_version_;
return create_and_connect_socket(socket, error);
}
@@ -15575,6 +15632,28 @@ inline void ClientImpl::disconnect(bool gracefully) {
close_socket(socket_);
}
inline bool ClientImpl::socket_reusable() {
if (!socket_.is_open()) { return false; }
auto is_alive = detail::is_socket_alive(socket_.sock);
#ifdef CPPHTTPLIB_SSL_ENABLED
if (is_alive && is_ssl() && tls::is_peer_closed(socket_.ssl, socket_.sock)) {
is_alive = false;
}
#endif
if (!is_alive) {
// Peer seems gone — non-graceful shutdown to avoid SIGPIPE.
disconnect(/*gracefully=*/false);
return false;
}
if (is_ssl() && socket_tls_settings_version_ != tls_settings_version_) {
disconnect(/*gracefully=*/true);
return false;
}
return true;
}
inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
Response &res,
bool skip_100_continue) const {
@@ -15652,25 +15731,7 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
// socket.
socket_should_be_closed_when_request_is_done_ = false;
auto is_alive = false;
if (socket_.is_open()) {
is_alive = detail::is_socket_alive(socket_.sock);
#ifdef CPPHTTPLIB_SSL_ENABLED
if (is_alive && is_ssl()) {
if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
is_alive = false;
}
}
#endif
if (!is_alive) {
// Peer seems gone — non-graceful shutdown to avoid SIGPIPE.
disconnect(/*gracefully=*/false);
}
}
if (!is_alive) {
if (!socket_reusable()) {
if (!ensure_socket_connection(socket_, error)) { return false; }
{
@@ -15833,20 +15894,7 @@ inline ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
{
std::lock_guard<std::mutex> guard(socket_mutex_);
auto is_alive = false;
if (socket_.is_open()) {
is_alive = detail::is_socket_alive(socket_.sock);
#ifdef CPPHTTPLIB_SSL_ENABLED
if (is_alive && is_ssl()) {
if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
is_alive = false;
}
}
#endif
if (!is_alive) { disconnect(/*gracefully=*/false); }
}
if (!is_alive) {
if (!socket_reusable()) {
if (!ensure_socket_connection(socket_, handle.error)) {
handle.response.reset();
return handle;
@@ -16113,6 +16161,15 @@ inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,
if (!is_space_or_tab(*p) && !fields::is_field_vchar(*p)) { return -1; }
}
// The bytes around the data are no payload, so they are counted on their
// own, as Go does: each chunk may spend 16 bytes and twice its data on
// them, which lets a stream send a byte at a time, and what goes past that
// in padding, leading zeros and extensions builds up to one header field.
overhead += lr.size() + 2;
overhead = overhead > 16 ? overhead - 16 : 0;
overhead = chunk_len > overhead / 2 ? 0 : overhead - 2 * chunk_len;
if (overhead > CPPHTTPLIB_HEADER_MAX_LENGTH) { return -1; }
if (chunk_len == 0) {
chunk_remaining = 0;
finished = true;
@@ -18054,11 +18111,15 @@ inline void ClientImpl::set_proxy_digest_auth(const std::string &username,
}
inline void ClientImpl::enable_server_certificate_verification(bool enabled) {
if (server_certificate_verification_ == enabled) { return; }
server_certificate_verification_ = enabled;
tls_settings_changed();
}
inline void ClientImpl::enable_server_hostname_verification(bool enabled) {
if (server_hostname_verification_ == enabled) { return; }
server_hostname_verification_ = enabled;
tls_settings_changed();
}
inline void ClientImpl::enable_system_ca(bool enabled) {
@@ -19217,25 +19278,32 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
tls::free_ca_store(ca_cert_store);
}
custom_ca_loaded_ = true;
tls_settings_changed();
}
inline void
SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {
if (!ctx_) { return; }
server_certificate_verifier_ = verifier;
// Under the lock that creating a session from ctx_ takes
std::lock_guard<std::mutex> guard(ctx_mutex_);
tls::set_verify_callback(ctx_, std::move(verifier));
{
// Under the lock that creating a session from ctx_ takes
std::lock_guard<std::mutex> guard(ctx_mutex_);
tls::set_verify_callback(ctx_, std::move(verifier));
}
tls_settings_changed();
}
inline void SSLClient::set_session_verifier(
std::function<SSLVerifierResponse(tls::session_t)> verifier) {
session_verifier_ = std::move(verifier);
tls_settings_changed();
}
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
inline void SSLClient::enable_windows_certificate_verification(bool enabled) {
if (enable_windows_cert_verification_ == enabled) { return; }
enable_windows_cert_verification_ = enabled;
tls_settings_changed();
}
#endif
@@ -19250,6 +19318,7 @@ inline void SSLClient::load_ca_cert_store(const char *ca_cert,
tls::load_ca_pem(ctx_, ca_cert, size);
}
custom_ca_loaded_ = true;
tls_settings_changed();
}
inline bool SSLClient::load_certs() {
@@ -21966,24 +22035,23 @@ inline ca_store_t create_ca_store(const char *pem, size_t len) {
if (!detail::ensure_mbedtls_psa_crypto()) { return nullptr; }
#endif
if (!pem) { return nullptr; }
// mbedtls_x509_crt_parse expects null-terminated PEM
std::string pem_str(pem, len);
auto *ca_chain = new (std::nothrow) mbedtls_x509_crt;
if (!ca_chain) { return nullptr; }
mbedtls_x509_crt_init(ca_chain);
// mbedtls_x509_crt_parse expects null-terminated PEM
int ret = mbedtls_x509_crt_parse(ca_chain,
reinterpret_cast<const unsigned char *>(pem),
len + 1); // +1 for null terminator
auto ret = mbedtls_x509_crt_parse(
ca_chain, reinterpret_cast<const unsigned char *>(pem_str.c_str()),
pem_str.size() + 1);
if (ret != 0) {
// Try without +1 in case PEM is already null-terminated
ret = mbedtls_x509_crt_parse(
ca_chain, reinterpret_cast<const unsigned char *>(pem), len);
if (ret != 0) {
mbedtls_x509_crt_free(ca_chain);
delete ca_chain;
return nullptr;
}
mbedtls_x509_crt_free(ca_chain);
delete ca_chain;
return nullptr;
}
return static_cast<ca_store_t>(ca_chain);
+304 -35
View File
@@ -2742,6 +2742,31 @@ TEST(RangeTest, FromHTTPBin_Online) {
}
}
TEST(RangeTest, IgnoredForPost) {
Server svr;
svr.Post("/", [](const Request &, Response &res) {
res.set_content("abcdefg", "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
ASSERT_GT(port, 0);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
for (auto range : {"bytes=1-2", "bytes=x"}) {
auto res = cli.Post("/", Headers{{"Range", range}});
ASSERT_TRUE(res) << range << ": " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status) << range;
EXPECT_EQ("abcdefg", res->body) << range;
}
}
TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
auto host = "unresolvableaddress.local";
auto path = std::string{"/"};
@@ -8073,6 +8098,25 @@ TEST_F(ServerTest, RejectsControlCharacterInChunkExtension) {
"b\r\ndech\r\nf\r\nunked post body\r\n0\r\n\r\n");
}
// The bytes around chunk data are no payload, so they cannot ride on the
// payload limit
TEST_F(ServerTest, RejectsChunkOverheadPastItsLimit) {
// Small enough that the whole request is sent before the server turns it
// down, yet past the limit by the last chunks
auto pad = std::string(CPPHTTPLIB_HEADER_MAX_LENGTH / 16, 'x');
auto zeros = std::string(CPPHTTPLIB_HEADER_MAX_LENGTH / 16, '0');
auto spaces = std::string(CPPHTTPLIB_HEADER_MAX_LENGTH / 16, ' ');
for (const auto &size_line : {"1;" + pad, zeros + "1", "1" + spaces}) {
SCOPED_TRACE(size_line.substr(0, 2));
std::string body;
for (auto c : std::string("dechunked post body")) {
body += size_line + "\r\n" + c + "\r\n";
}
body += "0\r\n\r\n";
expect_chunked_body_rejected(cli_, body.c_str());
}
}
TEST_F(ServerTest, AcceptsChunkExtension) {
expect_chunked_body_status(cli_,
"4;name=value\r\ndech\r\n"
@@ -11125,6 +11169,37 @@ TEST(HeadersOrderTest, SentFieldsKeepTheirOrder) {
EXPECT_EQ("second=2", res->get_header_value("Set-Cookie", "", 1));
}
// A stream may send a byte at a time with a small extension on each chunk,
// however long it runs
TEST(ChunkOverheadTest, AcceptsSmallOverheadOnEveryChunk) {
Server svr;
svr.Post("/count", [](const Request &req, Response &res) {
res.set_content(std::to_string(req.body.size()), "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
ASSERT_LT(0, port);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
std::string req = std::string("POST /count HTTP/1.1\r\nHost: ") + HOST +
"\r\nTransfer-Encoding: chunked\r\n"
"Connection: close\r\n\r\n";
for (auto i = 0; i < 4096; i++) {
req += "1;a=b\r\nx\r\n";
}
req += "0\r\n\r\n";
std::string res;
ASSERT_TRUE(send_request(5, req, &res, port));
EXPECT_EQ(0u, res.find("HTTP/1.1 200 OK\r\n"));
EXPECT_EQ("4096", res.substr(res.size() - 4));
}
TEST(ServerResponseSplittingTest, ChunkedTrailerCRLFInjection) {
Server svr;
svr.Get("/injected-trailer", [&](const Request & /*req*/, Response &res) {
@@ -11183,48 +11258,66 @@ TEST(ServerResponseSplittingTest, ChunkedTrailerCRLFInjection) {
}
TEST(ServerResponseSplittingTest, ResponseHeaderCRLFInjection) {
Server svr;
svr.Get("/injected-header", [&](const Request & /*req*/, Response &res) {
// res.headers is a public field an application can populate directly,
// bypassing set_header()'s validation (e.g. reflecting a request value).
// A valid header that must survive.
res.headers.emplace("X-Safe", "safe");
// Attacker-controlled value carrying CR/LF (response splitting).
res.headers.emplace("X-Reflected",
"legit\r\nInjected-Header: pwned\r\nX-Evil: also");
// Attacker-controlled name carrying CR/LF.
res.headers.emplace("X-Bad\r\nSet-Cookie: a=1", "x");
res.set_content("body", "text/plain");
});
// A custom header writer gets only the fields the default one writes
auto verbatim_writer = [](Stream &strm, Headers &hdrs) -> ssize_t {
std::string s;
for (const auto &h : hdrs) {
s += h.first + ": " + h.second + "\r\n";
}
s += "\r\n";
return strm.write(s.data(), s.size());
};
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
for (auto custom_writer : {false, true}) {
SCOPED_TRACE(custom_writer ? "custom header writer" : "default writer");
Server svr;
if (custom_writer) { svr.set_header_writer(verbatim_writer); }
svr.set_post_routing_handler([](const Request &, Response &res) {
res.headers.emplace("X-Late", "late\r\nInjected-Late: pwned");
});
svr.Get("/injected-header", [&](const Request & /*req*/, Response &res) {
// res.headers is a public field an application can populate directly,
// bypassing set_header()'s validation (e.g. reflecting a request value).
// A valid header that must survive.
res.headers.emplace("X-Safe", "safe");
// Attacker-controlled value carrying CR/LF (response splitting).
res.headers.emplace("X-Reflected",
"legit\r\nInjected-Header: pwned\r\nX-Evil: also");
// Attacker-controlled name carrying CR/LF.
res.headers.emplace("X-Bad\r\nSet-Cookie: a=1", "x");
res.set_content("body", "text/plain");
});
svr.wait_until_ready();
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
const std::string req = std::string("GET /injected-header HTTP/1.1\r\n") +
"Host: " + HOST +
"\r\n"
"Connection: close\r\n"
"\r\n";
svr.wait_until_ready();
std::string res;
ASSERT_TRUE(send_request(5, req, &res));
const std::string req = std::string("GET /injected-header HTTP/1.1\r\n") +
"Host: " + HOST +
"\r\n"
"Connection: close\r\n"
"\r\n";
// The injected fields must not appear anywhere in the raw response.
EXPECT_EQ(std::string::npos, res.find("Injected-Header"));
EXPECT_EQ(std::string::npos, res.find("X-Evil"));
EXPECT_EQ(std::string::npos, res.find("Set-Cookie"));
std::string res;
ASSERT_TRUE(send_request(5, req, &res));
// Invalid headers are dropped entirely, matching set_header().
EXPECT_EQ(std::string::npos, res.find("X-Reflected:"));
// The injected fields must not appear anywhere in the raw response.
EXPECT_EQ(std::string::npos, res.find("Injected-Header"));
EXPECT_EQ(std::string::npos, res.find("X-Evil"));
EXPECT_EQ(std::string::npos, res.find("Set-Cookie"));
EXPECT_EQ(std::string::npos, res.find("Injected-Late"));
// The valid header still passes through.
EXPECT_NE(std::string::npos, res.find("X-Safe: safe"));
// Invalid headers are dropped entirely, matching set_header().
EXPECT_EQ(std::string::npos, res.find("X-Reflected:"));
// The valid header still passes through.
EXPECT_NE(std::string::npos, res.find("X-Safe: safe"));
}
}
// Forward declaration: in split builds split.py strips `inline` and moves the
@@ -14842,6 +14935,45 @@ TEST(ContentEncodingTest, PreEncodedResponseIsNotCompressedAgain) {
EXPECT_EQ("Hello World!", res->body);
}
// set_compress(false) sends every handler response as it is, buffered or
// streamed
TEST(ContentEncodingTest, SetCompressFalseSendsIdentity) {
const std::string text(1000, 'a');
Server svr;
svr.set_compress(false);
svr.Get("/buffered", [&](const Request &, Response &res) {
res.set_content(text, "text/plain");
});
svr.Get("/streamed", [&](const Request &, Response &res) {
res.set_chunked_content_provider("text/plain", [&](size_t, DataSink &sink) {
sink.write(text.data(), text.size());
sink.done();
return true;
});
});
auto port = svr.bind_to_any_port(HOST);
ASSERT_LT(0, port);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
cli.set_decompress(false);
for (auto path : {"/buffered", "/streamed"}) {
SCOPED_TRACE(path);
auto res = cli.Get(path, Headers{{"Accept-Encoding", "gzip"}});
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_FALSE(res->has_header("Content-Encoding"));
EXPECT_EQ(text, res->body);
}
}
// A chunked provider settles its coding where the headers are written and
// reuses it at write time. Both ends of that have to hold: a handler's own
// Content-Encoding suppresses the coding, and a response without one is still
@@ -18238,6 +18370,94 @@ TEST(SSLClientTest, EmptyCustomCaKeepsSystemCasOut) {
}
#endif
// A TLS setting changed between requests applies to the next one, not just to
// the next connection
TEST(SSLClientTest, TlsSettingChangeClosesKeptAliveConnection) {
SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
svr.Get("/", [](const Request &req, Response &res) {
res.set_content(std::to_string(req.remote_port), "text/plain");
});
auto port = svr.bind_to_any_port("127.0.0.1");
ASSERT_LT(0, port);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
SSLClient cli("127.0.0.1", port);
cli.set_keep_alive(true);
cli.set_ca_cert_path(CLIENT_CA_CERT_FILE); // does not trust cert2.pem
cli.enable_server_certificate_verification(false);
// The port the request went out from, so a new one means a new connection
auto client_port = [&] {
auto res = cli.Get("/");
return res ? res->body : std::string();
};
auto last = client_port();
ASSERT_FALSE(last.empty());
// Setting a value it already has keeps the connection
cli.enable_server_certificate_verification(false);
cli.enable_server_hostname_verification(true);
EXPECT_EQ(last, client_port());
cli.enable_server_certificate_verification(true);
auto res = cli.Get("/");
EXPECT_FALSE(res);
EXPECT_EQ(Error::SSLServerVerification, res.error());
std::string ca;
read_file(SERVER_CERT2_FILE, ca);
cli.load_ca_cert_store(ca.c_str(), ca.size());
last = client_port();
ASSERT_FALSE(last.empty());
std::vector<std::function<void()>> setters = {
[&] { cli.enable_server_hostname_verification(false); },
[&] { cli.load_ca_cert_store(ca.c_str(), ca.size()); },
[&] {
cli.set_ca_cert_store(tls::create_ca_store(ca.c_str(), ca.size()));
},
[&] {
cli.set_server_certificate_verifier(
[](const tls::VerifyContext &) { return true; });
},
[&] {
cli.set_session_verifier(
[](tls::session_t) { return SSLVerifierResponse::NoDecisionMade; });
},
};
for (const auto &setter : setters) {
setter();
auto next = client_port();
ASSERT_FALSE(next.empty());
EXPECT_NE(last, next);
last = next;
}
cli.set_session_verifier(
[](tls::session_t) { return SSLVerifierResponse::CertificateRejected; });
auto handle = cli.open_stream("GET", "/");
EXPECT_FALSE(handle.is_valid());
EXPECT_EQ(Error::SSLServerVerification, handle.error);
}
// A PEM given to create_ca_store() needs no NUL after it
TEST(SSLClientTest, CreateCaStoreFromPemWithoutNul) {
std::string pem;
read_file(SERVER_CERT2_FILE, pem);
auto buf = pem + "X";
auto store = tls::create_ca_store(buf.data(), pem.size());
EXPECT_TRUE(store != nullptr);
SSLClient cli("localhost");
cli.set_ca_cert_store(store);
}
// enable_system_ca(false) prevents system CA loading entirely
TEST(SSLClientTest, DisableSystemCa_Online) {
SSLClient cli("google.com");
@@ -22367,6 +22587,55 @@ TEST(ForwardedHeadersTest, NoForwardedHeaders) {
observed_remote_addr == "127.0.0.1");
}
// A server listening on both IPv4 and IPv6 sees an IPv4 peer as an
// IPv4-mapped IPv6 address, which still matches the proxy's IPv4 address
TEST(ForwardedHeadersTest, IPv4MappedPeerMatchesIPv4Proxy) {
Server svr;
svr.set_trusted_proxies({"127.0.0.1"});
svr.Get("/ip", [](const Request &req, Response &res) {
res.set_content(req.remote_addr, "text/plain");
});
auto port = svr.bind_to_any_port("::");
if (port <= 0) { GTEST_SKIP() << "no IPv6 on this host"; }
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli("127.0.0.1", port);
auto res = cli.Get("/ip", Headers{{"X-Forwarded-For", "198.51.100.23"}});
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("198.51.100.23", res->body);
}
TEST(ForwardedHeadersTest, IPv4MappedHopMatchesIPv4Proxy) {
Server svr;
svr.set_trusted_proxies({"192.0.2.45", "::1", "127.0.0.1"});
svr.Get("/ip", [](const Request &req, Response &res) {
res.set_content(req.remote_addr, "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
ASSERT_GT(port, 0);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
auto res = cli.Get(
"/ip", Headers{{"X-Forwarded-For", "198.51.100.23, ::ffff:192.0.2.45"}});
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("198.51.100.23", res->body);
}
TEST(ForwardedHeadersTest, SingleTrustedProxy_UsesIPBeforeTrusted) {
Server svr;