Compare commits

..
19 Commits
Author SHA1 Message Date
yhirose cf3693cb5c Release v0.59.0 2026-10-02 22:06:39 -04:00
yhirose 4fd9ae8f42 Serve pipelined requests without waiting for the keep-alive timeout
A client may pipeline its requests (RFC 9112 9.3.2). The server read the
following request(s) into a per-request SocketStream buffer, discarded them
with the stream after the first response, and then waited in keep_alive()
for socket data that never came, closing the connection after the
keep-alive timeout. Over TLS the bytes stayed decrypted in the TLS library,
where keep_alive() could not see them either.

Create one stream per connection and serve a request that is already
buffered (Stream::is_readable()) without waiting in keep_alive().

Keeping the buffer means an extra CRLF that some clients send after a
request body is now parsed as the next request line, which answered 400
and closed the connection. Ignore one empty line before the request-line,
as RFC 9112 2.2 recommends.

Fixes #2599
2026-10-02 21:47:18 -04:00
yhirose 3d40dfc727 Fix SSE parsing of CRLF field names and data-less events
A field line without a colon kept the \r of a CRLF line ending in its
name, so "data\r\n" was not recognized. Strip the \r once per line
before parsing instead of from each value.

An event without a data field was neither dispatched nor cleared, so
its event type leaked into the next event and its id only reached
last_event_id after a later event was dispatched. Reset the message on
every blank line and record the id even when nothing is dispatched.

The parsing tests exercised a copy of parse_sse_line that had drifted
from the real one. Run them through SSEClient against a local server
instead, and add regression tests for the cases above.
2026-10-02 21:38:39 -04:00
metsw24-max dd71728110 escape quoted-string auth-params in make_digest_authentication_header (#2597) 2026-10-02 21:29:35 -04:00
DosX 7255a7e979 Preserve empty SSE data fields (#2594)
SSE events may contain an empty data field, and a data field without a colon also has an empty value. Track whether a data field was seen separately from the accumulated payload so empty events are dispatched and leading empty lines are preserved. Add an integration regression test for both forms.
2026-10-02 21:17:47 -04:00
yhirose 8a3abfb597 Extract parse_int_in_range from parse_port
parse_port and the NO_PROXY CIDR prefix parsing each repeated the same
from_chars call, full-consumption check and range check. Move that into
a parse_int_in_range helper and use it in both places.
2026-10-02 21:15:14 -04:00
KBS 10aadd57f7 Reject trailing characters in URL port numbers (#2593)
parse_port checked only the error code of from_chars, which stops at
the first non-digit, so http://host:80abc was accepted as port 80, and
a redirect Location with such a port was followed. RFC 3986 defines
port as *DIGIT. Require the whole string to be consumed, as #2590 does
for quality values.
2026-10-02 21:07:34 -04:00
yhirose 43863e1f67 Make CryptoAPI the only chain verifier when Windows verification is on (#2604)
Every backend loads the Windows ROOT and CA stores, but Windows adds a
root to them only when CryptoAPI needs it to build a chain. On a machine
that had not needed a root yet, the backend rejected the chain before
the CryptoAPI check ran, so Windows never fetched the root (for example
OpenSSL error 20 for accounts.spotify.com under Starfield Root G2).

With Windows verification enabled, the backend's chain verdict is no
longer used. Mbed TLS and wolfSSL skip chain verification during the
handshake, and the post-handshake verify result is ignored. The
CryptoAPI check becomes mandatory: a leaf that cannot be encoded now
fails the connection instead of skipping the check, and the chain must
allow server authentication, which the backends used to check.

A server certificate verifier works on the backend's chain verification,
so when one is set the backend still decides and CryptoAPI only adds its
own check, as before.

ClientCertMissing now disables hostname verification: cert.pem does not
match HOST, and on Windows the hostname check fails before the chain
check.

Refs #2596
2026-10-02 20:48:47 -04:00
yhirose 0db1df7cf2 Pass the server's intermediates to Windows certificate verification (#2602)
CryptoAPI got only the leaf, so it fetched an issuer from the leaf's AIA
URL instead of using the intermediates the server sent. For
accounts.spotify.com that issuer chains to Certainly Root R1, which
Windows does not trust, while the server's own chain ends at Starfield
Root G2.

Add tls::get_peer_certs(), which returns the certificates the peer sent
in the same way get_ca_certs() returns the CA certificates, for every
backend. The CryptoAPI check puts them into a memory store that it
passes to CertGetCertificateChain(), skipping any certificate that
cannot be added. wolfSSL keeps the received chain only when built with
SESSION_CERTS; without it, CryptoAPI still gets the leaf alone.

Refs #2596
2026-10-02 20:48:10 -04:00
yhirose 639391ad7f Reject an invalid Content-Length and honor Connection: close
A request whose Content-Length was present but not a valid decimal length
(e.g. "42, 42", "+42", "0x2e" or empty) was treated as having no body
unless a handler read it: no 400 was returned, the body was not drained,
and the bytes after the header block were parsed as the next request on
the keep-alive connection. Reject such a request with 400 and close the
connection before routing, as RFC 9112 Section 6.3 requires.

The server also kept reading after a response that announced
Connection: close. A rejected request line or header block left the rest
of the message to be parsed as a new request, and an error response to a
bodyless request did the same with whatever followed. Close the
connection whenever the final response carries Connection: close
(RFC 9112 Section 9.6), and mark the two request-head rejection paths
closed explicitly as the other rejection paths already do.
2026-10-02 00:22:24 -04:00
yhirose 0715c2739e Enforce a minimum SSE reconnect wait to avoid a busy loop (#2592)
SSEClient::wait_for_reconnect() sleeps in 100ms steps until the
reconnect interval has elapsed. With an interval of 0 (for example
"retry: 0" from the server, or set_reconnect_interval(0)) it never
slept at all, so a server that sends "retry: 0" and closes the stream
made the client reconnect in a tight loop. set_max_reconnect_attempts()
does not stop this either, because each successful connection resets
the attempt counter.

Always wait at least one step (100ms). Intervals of 1-99ms already
waited 100ms because of the step size, so only 0 and negative values
change behavior.
2026-09-28 17:25:37 -04:00
KBS 3330d0eb06 Ignore an SSE retry field that is not all digits (#2591)
parse_sse_line checked only the error code of from_chars, which accepts
a leading '-' and stops at the first non-digit, so retry: -1 made the
client reconnect without waiting and retry: 10s set 10 ms. The SSE spec
ignores a retry value that is not all ASCII digits.
2026-09-28 15:31:20 -04:00
yhirose c1c2b1f4b4 Apply the request-target check to the client and encode control chars
Share the server's request-target check as fields::is_request_target()
and use it in write_request_line too. The client previously used
is_field_value(), which let an embedded SP or HTAB through.

encode_path() only escaped CR/LF among the control characters, so with
path encoding enabled a path like "/a\tb" would now be rejected instead
of sent. Percent-encode every control character (0x00-0x1F, 0x7F).
2026-09-28 03:37:11 -04:00
yhirose e11dbec7b3 Reject control characters in the request-target
RFC 9112 §3.2 does not allow control characters in the request-target,
and §2.2 requires a bare CR to be treated as invalid. parse_request_line
accepted them, so e.g. "GET /a\rb HTTP/1.1" was routed normally. Reject
any byte that is not VCHAR or obs-text with 400 Bad Request. obs-text is
still allowed since some clients send raw UTF-8 in the target.
2026-09-27 23:35:09 -04:00
yhirose 174bce5ccf Escape request data in the docker server's access and error logs
req.path is percent-decoded, so a request like GET /%0D%0A... put a
literal CR/LF into the NGINX-style log lines and let a client forge
extra entries. Log the raw req.target (matching NGINX's $request) and
escape '"', '\', control and non-ASCII bytes as \xHH the way NGINX
does. Also note in the README logging section that req.path may
contain control characters and should be escaped before logging.
2026-09-27 22:50:26 -04:00
DosX 57c4f7f385 Reject trailing characters in HTTP quality values (#2590)
parse_quality accepted values such as q=0.5junk because it checked only the conversion error and ignored the returned end pointer. Require the numeric parser to consume the complete q parameter so malformed Accept values are rejected and invalid Accept-Encoding weights are ignored. Add regression cases for both headers.
2026-09-27 19:20:14 -04:00
yhirose 2fb2dbbe1e Send small static files with the headers and large bodies without a copy (#2589)
A file served from a mount point or through set_file_content() left in two
writes, one for the status line and headers and one for the body, because the
body came from a content provider. A small file is now read into the header
buffer so the whole response leaves in a single write. Only file-backed
providers are coalesced this way: a user-supplied provider may produce its data
over time, and holding the headers back until it finishes would stall the
client.

A large set_content() body was copied into the header buffer before being
sent. A body of CPPHTTPLIB_SEND_BUFSIZ or more is now written directly after
the headers, which saves the copy at the cost of one extra write.
2026-09-26 22:19:15 -04:00
VecSzn 8b6ab24159 Resolve relative Location references on redirect (#2586) 2026-09-26 19:49:21 -04:00
Tobias WallnerandTobias Wallner 5a202d3d5f Added a feature test to auto enable/disable CPPHTTPLIB_USE_NON_BLOCKI… (#2578)
* Added a feature test to auto enable/disable CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO

* turned status: WARNING 'GetAddrInfoExCancel is unavailable; disabling non-blocking getaddrinfo.' into a warning

* added ws2_32 for the GetAddrInfoExCancel. this catches previous false negatives

---------

Co-authored-by: Tobias Wallner <tobias.wallner@qtlabs.at>
2026-09-25 15:20:00 -04:00
7 changed files with 1256 additions and 420 deletions
+21 -3
View File
@@ -15,7 +15,7 @@
* HTTPLIB_REQUIRE_BROTLI (default off)
* HTTPLIB_REQUIRE_ZSTD (default off)
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on when supported)
* HTTPLIB_COMPILE (default off)
* HTTPLIB_INSTALL (default on)
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
@@ -181,6 +181,24 @@ if(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES)
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
endif()
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
if(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO AND WIN32)
include(CheckCXXSymbolExists)
set(_httplib_cmake_required_definitions ${CMAKE_REQUIRED_DEFINITIONS})
set(_httplib_cmake_required_libraries ${CMAKE_REQUIRED_LIBRARIES})
list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_WIN32_WINNT=0x0A00)
list(APPEND CMAKE_REQUIRED_LIBRARIES ws2_32)
check_cxx_symbol_exists(GetAddrInfoExCancel "winsock2.h;ws2tcpip.h" HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
set(CMAKE_REQUIRED_DEFINITIONS ${_httplib_cmake_required_definitions})
set(CMAKE_REQUIRED_LIBRARIES ${_httplib_cmake_required_libraries})
unset(_httplib_cmake_required_definitions)
unset(_httplib_cmake_required_libraries)
if(NOT HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO FALSE)
message(WARNING "GetAddrInfoExCancel is unavailable; disabling non-blocking getaddrinfo.")
endif()
endif()
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
@@ -367,7 +385,7 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
# Needed for API from MacOS Security framework
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
# Needed for non-blocking getaddrinfo on MacOS
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
# Can't put multiple targets in a single generator expression or it bugs out.
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
@@ -390,7 +408,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_SUPPORT>
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
$<$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
)
# CMake configuration files installation directory
+1 -1
View File
@@ -69,7 +69,7 @@ sse.on_error([](httplib::Error err) { });
#### Configuration
```cpp
// Set reconnect interval (default: 3000ms)
// Set reconnect interval (default: 3000ms, minimum: 100ms)
sse.set_reconnect_interval(5000);
// Set max reconnect attempts (default: 0 = unlimited)
+4 -1
View File
@@ -230,7 +230,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` |
| Windows | Verifies certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend, with revocation checking. 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, verification can also be disabled at runtime:
@@ -452,6 +452,9 @@ svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
});
```
> [!NOTE]
> `req.path` is percent-decoded and may contain control characters such as CR/LF. Escape request data before writing it to a log file (see [docker/main.cc](docker/main.cc) for an example).
#### Pre-compression Logging
You can also set a pre-compression logger to capture request/response data before compression is applied:
+26 -6
View File
@@ -48,6 +48,23 @@ std::string get_error_time_format() {
return ss.str();
}
// Escape a value for a log line the way NGINX does: '"', '\\', control
// bytes and non-ASCII bytes become \xHH. Request fields are attacker-controlled
// (e.g. a raw CR in the request target or a decoded %0D%0A in req.path), so
// writing them verbatim would let a client forge extra log lines.
std::string escape_log(const std::string &s) {
std::string out;
out.reserve(s.size());
for (unsigned char c : s) {
if (c == '"' || c == '\\' || c < 0x20 || c >= 0x7f) {
out += std::format("\\x{:02X}", c);
} else {
out += static_cast<char>(c);
}
}
return out;
}
// NGINX Combined log format:
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
// "$http_referer" "$http_user_agent"
@@ -55,7 +72,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
std::string remote_user =
"-"; // cpp-httplib doesn't have built-in auth user tracking
auto time_local = get_time_format();
auto request = std::format("{} {} {}", req.method, req.path, req.version);
// $request is the original request line, so log the raw target rather than
// the percent-decoded req.path.
auto request = std::format("{} {} {}", req.method, req.target, req.version);
auto status = res.status;
auto body_bytes_sent = res.body.size();
auto http_referer = req.get_header_value("Referer");
@@ -64,9 +83,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
if (http_user_agent.empty()) http_user_agent = "-";
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
req.remote_addr, remote_user, time_local, request,
status, body_bytes_sent, http_referer,
http_user_agent)
req.remote_addr, remote_user, time_local,
escape_log(request), status, body_bytes_sent,
escape_log(http_referer), escape_log(http_user_agent))
<< std::endl;
}
@@ -79,14 +98,15 @@ void nginx_error_logger(const Error &err, const Request *req) {
if (req) {
auto request =
std::format("{} {} {}", req->method, req->path, req->version);
std::format("{} {} {}", req->method, req->target, req->version);
auto host = req->get_header_value("Host");
if (host.empty()) host = "-";
std::cerr << std::format("{} [{}] {}, client: {}, request: "
"\"{}\", host: \"{}\"",
time_local, level, to_string(err),
req->remote_addr, request, host)
req->remote_addr, escape_log(request),
escape_log(host))
<< std::endl;
} else {
// If no request context, just log the error
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.58.0"
version = "0.59.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+398 -142
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.58.0"
#define CPPHTTPLIB_VERSION_NUM "0x003a00"
#define CPPHTTPLIB_VERSION "0.59.0"
#define CPPHTTPLIB_VERSION_NUM "0x003b00"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -831,14 +831,21 @@ inline from_chars_result<double> from_chars(const char *first, const char *last,
return {p, std::errc{}};
}
inline bool parse_port(const char *s, size_t len, int &port) {
inline bool parse_int_in_range(const char *s, size_t len, int lo, int hi,
int &out) {
int val = 0;
auto r = from_chars(s, s + len, val);
if (r.ec != std::errc{} || val < 1 || val > 65535) { return false; }
port = val;
if (r.ec != std::errc{} || r.ptr != s + len || val < lo || val > hi) {
return false;
}
out = val;
return true;
}
inline bool parse_port(const char *s, size_t len, int &port) {
return parse_int_in_range(s, len, 1, 65535, port);
}
inline bool parse_port(const std::string &s, int &port) {
return parse_port(s.data(), s.size(), port);
}
@@ -939,6 +946,48 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
return true;
}
// Resolves a relative-path or query-only Location value against the path of
// the request being redirected (RFC 3986 section 5.2). Absolute URIs and
// references starting with '/' are returned unchanged.
inline std::string resolve_relative_location(const std::string &location,
const std::string &base) {
if (location.empty() || location[0] == '/') { return location; }
// A ':' in the first segment means the value has a scheme.
if (location.find(':') < location.find_first_of("/?#")) { return location; }
if (location[0] == '#') { return base.substr(0, base.find('#')) + location; }
auto base_path = base.substr(0, base.find_first_of("?#"));
if (location[0] == '?') { return base_path + location; }
if (base_path.empty() || base_path[0] != '/') { base_path = "/"; }
auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location;
// Remove "." and ".." segments from the merged path.
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
std::string path;
size_t i = 0;
while (i < path_end) {
auto next = (std::min)(merged.find('/', i + 1), path_end);
auto segment = merged.substr(i + 1, next - i - 1);
auto is_last = next == path_end;
if (segment == "." || segment == "..") {
if (segment == "..") {
path.erase((std::min)(path.rfind('/'), path.size()));
}
if (is_last) { path += '/'; }
} else {
path += '/';
path += segment;
}
i = next;
}
if (path.empty()) { path = "/"; }
return path + merged.substr(path_end);
}
} // namespace detail
enum class SSLVerifierResponse {
@@ -1842,6 +1891,7 @@ struct Response {
ContentProvider content_provider_;
ContentProviderResourceReleaser content_provider_resource_releaser_;
bool is_chunked_content_provider_ = false;
bool is_file_content_provider_ = false;
bool content_provider_success_ = false;
std::string file_content_path_;
std::string file_content_content_type_;
@@ -3399,6 +3449,9 @@ private:
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
bool enable_windows_cert_verification_ = true;
// Like ca_cert_store_set_, tracks what ctx_ cannot report back: whether
// set_server_certificate_verifier() installed a verifier.
bool server_certificate_verifier_set_ = false;
#endif
friend class ClientImpl;
@@ -3914,6 +3967,7 @@ bool is_field_vchar(char c);
bool is_field_content(const std::string &s);
bool is_field_value(const std::string &s);
bool is_field_valid(const std::string &name, const std::string &value);
bool is_request_target(const std::string &s);
} // namespace fields
} // namespace detail
@@ -4322,7 +4376,8 @@ public:
void stop();
private:
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms);
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms,
bool &has_data);
void run_event_loop();
void dispatch_event(const SSEMessage &msg);
bool should_reconnect(int count) const;
@@ -4832,30 +4887,23 @@ inline void SSEClient::stop() {
}
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
int &retry_ms) {
int &retry_ms, bool &has_data) {
// Blank line signals end of event
if (line.empty() || line == "\r") { return true; }
if (line.empty()) { return true; }
// Lines starting with ':' are comments (ignored)
if (!line.empty() && line[0] == ':') { return false; }
if (line[0] == ':') { return false; }
// Find the colon separator
auto colon_pos = line.find(':');
if (colon_pos == std::string::npos) {
// Line with no colon is treated as field name with empty value
return false;
}
auto field = line.substr(0, colon_pos);
std::string value;
// Value starts after colon, skip optional single space
if (colon_pos + 1 < line.size()) {
if (colon_pos != std::string::npos && colon_pos + 1 < line.size()) {
auto value_start = colon_pos + 1;
if (line[value_start] == ' ') { value_start++; }
value = line.substr(value_start);
// Remove trailing \r if present
if (!value.empty() && value.back() == '\r') { value.pop_back(); }
}
// Handle known fields
@@ -4863,14 +4911,16 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
msg.event = value;
} else if (field == "data") {
// Multiple data lines are concatenated with newlines
if (!msg.data.empty()) { msg.data += "\n"; }
if (has_data) { msg.data += "\n"; }
msg.data += value;
has_data = true;
} else if (field == "id") {
// Empty id is valid (clears the last event ID)
msg.id = value;
} else if (field == "retry") {
// Parse retry interval in milliseconds
{
// Per the SSE spec, a value that is not all ASCII digits is ignored.
if (detail::is_numeric(value)) {
int v = 0;
auto res =
detail::from_chars(value.data(), value.data() + value.size(), v);
@@ -4937,6 +4987,7 @@ inline void SSEClient::run_event_loop() {
// Event receiving loop
std::string buffer;
SSEMessage current_msg;
bool has_data = false;
while (running_.load() && result.next()) {
buffer.append(result.data(), result.size());
@@ -4950,18 +5001,25 @@ inline void SSEClient::run_event_loop() {
auto line = buffer.substr(line_start, newline_pos - line_start);
line_start = newline_pos + 1;
// Strip the \r of a CRLF line ending so that every field, including
// one without a colon, sees the same line
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
// Parse the line and check if event is complete
auto event_complete =
parse_sse_line(line, current_msg, reconnect_interval_ms_);
parse_sse_line(line, current_msg, reconnect_interval_ms_, has_data);
if (event_complete && !current_msg.data.empty()) {
// Update last_event_id for reconnection
if (event_complete) {
// Update last_event_id for reconnection, even for an event that
// has no data
if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
// Dispatch event to appropriate handler
dispatch_event(current_msg);
// An event without a data field is not dispatched
if (has_data) { dispatch_event(current_msg); }
// Reset the message for the next event either way
current_msg.clear();
has_data = false;
}
}
@@ -5006,11 +5064,15 @@ inline bool SSEClient::should_reconnect(int count) const {
}
inline void SSEClient::wait_for_reconnect() {
// Use small increments to check running_ flag frequently
// Use small increments to check running_ flag frequently.
// Always wait at least one increment, so that a zero interval (e.g.
// "retry: 0" from the server) cannot cause a busy reconnect loop.
const auto step_ms = 100;
auto interval_ms = (std::max)(reconnect_interval_ms_, step_ms);
auto waited = 0;
while (running_.load() && waited < reconnect_interval_ms_) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
waited += 100;
while (running_.load() && waited < interval_ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(step_ms));
waited += step_ms;
}
}
@@ -5071,6 +5133,9 @@ bool is_peer_closed(session_t session, socket_t sock);
// Certificate verification
cert_t get_peer_cert(const_session_t session);
// The certificates the peer sent, leaf first. Free each with free_cert(), and
// do not use them after free_session(), as with get_peer_cert().
size_t get_peer_certs(const_session_t session, std::vector<cert_t> &certs);
void free_cert(cert_t cert);
bool verify_hostname(cert_t cert, const char *hostname);
uint64_t hostname_mismatch_code();
@@ -5778,15 +5843,15 @@ inline std::string encode_path(const std::string &s) {
switch (s[i]) {
case ' ': result += "%20"; break;
case '+': result += "%2B"; break;
case '\r': result += "%0D"; break;
case '\n': result += "%0A"; break;
case '\'': result += "%27"; break;
case ',': result += "%2C"; break;
// case ':': result += "%3A"; break; // ok? probably...
case ';': result += "%3B"; break;
default:
auto c = static_cast<uint8_t>(s[i]);
if (c >= 0x80) {
// Control characters (incl. CR/LF) and non-ASCII bytes are not allowed
// in a request-target as-is.
if (c < 0x20 || c == 0x7f || c >= 0x80) {
result += '%';
char hex[4];
auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c);
@@ -6559,18 +6624,30 @@ inline bool keep_alive(const std::atomic<socket_t> &svr_sock, socket_t sock,
return false;
}
template <typename T>
inline bool
process_server_socket_core(const std::atomic<socket_t> &svr_sock, socket_t sock,
size_t keep_alive_max_count,
time_t keep_alive_timeout_sec, T callback) {
// `has_buffered_request` reports whether the connection's stream already holds
// bytes of the next request. A client may pipeline its requests (RFC 9112
// 9.3.2), so reading one request can pull the start of the next one into the
// stream's buffer; that request must be served without waiting for the socket
// to become readable again, since its bytes are no longer on the socket.
// `callback` is told whether keep_alive() has just seen the socket go readable.
template <typename P, typename T>
inline bool process_server_socket_core(const std::atomic<socket_t> &svr_sock,
socket_t sock,
size_t keep_alive_max_count,
time_t keep_alive_timeout_sec,
P has_buffered_request, T callback) {
assert(keep_alive_max_count > 0);
auto ret = false;
auto count = keep_alive_max_count;
while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) {
while (count > 0) {
auto socket_readable = false;
if (!has_buffered_request()) {
if (!keep_alive(svr_sock, sock, keep_alive_timeout_sec)) { break; }
socket_readable = true;
}
auto close_connection = count == 1;
auto connection_closed = false;
ret = callback(close_connection, connection_closed);
ret = callback(socket_readable, close_connection, connection_closed);
if (!ret || connection_closed) { break; }
count--;
}
@@ -6584,14 +6661,16 @@ process_server_socket(const std::atomic<socket_t> &svr_sock, socket_t sock,
time_t keep_alive_timeout_sec, time_t read_timeout_sec,
time_t read_timeout_usec, time_t write_timeout_sec,
time_t write_timeout_usec, T callback) {
// One stream per connection: its read buffer can already hold the start of
// the next, pipelined request.
SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
write_timeout_sec, write_timeout_usec);
return process_server_socket_core(
svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec,
[&](bool close_connection, bool &connection_closed) {
SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
write_timeout_sec, write_timeout_usec);
// process_server_socket_core() only gets here once keep_alive() has
// seen the socket go readable.
strm.set_readable_hint();
[&]() { return strm.is_readable(); },
[&](bool socket_readable, bool close_connection,
bool &connection_closed) {
if (socket_readable) { strm.set_readable_hint(); }
return callback(strm, close_connection, connection_closed);
});
}
@@ -7580,7 +7659,8 @@ inline bool parse_quality(const char *b, const char *e, std::string &token,
double v = 0.0;
auto res = from_chars(pb + r.first, pb + r.second, v);
if (res.ec != std::errc{} || v < 0.0 || v > 1.0) {
if (res.ec != std::errc{} || res.ptr != pb + r.second ||
v < 0.0 || v > 1.0) {
invalid = true;
return true;
}
@@ -8517,14 +8597,11 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
inline ssize_t write_request_line(Stream &strm, const std::string &method,
const std::string &path) {
// Neither the method nor the request target may carry CR/LF (or other
// control octets); otherwise a value smuggled into either splits the request
// line and injects headers or a whole request. The method must be a token
// (RFC 9110 Section 9.1), which also rejects an empty method and embedded
// spaces. The target gets the same field-value check that already guards
// header values in check_and_write_headers.
// Neither the method nor the request target may carry CR/LF, SP or other
// control octets; otherwise a value smuggled into either splits the request
// line and injects headers or a whole request.
if (!fields::is_token(method)) { return -1; }
if (!fields::is_field_value(path)) { return -1; }
if (!fields::is_request_target(path)) { return -1; }
std::string s = method;
s += ' ';
@@ -8746,9 +8823,9 @@ inline bool compress_content_provider(const ContentProvider &content_provider,
return cmp.compress(nullptr, 0, true, append);
}
// Serves `m` as the response body. `set_content_provider()` clears the coding,
// so recording it has to come after; keeping both here means a third
// file-serving path cannot get that order wrong.
// Serves `m` as the response body. `set_content_provider()` clears the coding
// and the file flag, so recording them has to come after; keeping all of it
// here means a third file-serving path cannot get that order wrong.
inline void set_file_content_provider(Response &res,
const std::shared_ptr<mmap> &m,
const std::string &content_type,
@@ -8760,6 +8837,7 @@ inline void set_file_content_provider(Response &res,
return true;
});
res.is_file_content_provider_ = true;
res.content_coding_ = encoding;
}
@@ -10051,6 +10129,19 @@ inline std::string unescape_quoted_pairs(const std::string &s) {
return out;
}
// Inverse of unescape_quoted_pairs: prepares a value to sit inside a
// quoted-string. RFC 9110 §5.6.4 requires a literal '\' or '"' to be sent as a
// quoted-pair, so the recipient recovers the original value.
inline std::string escape_quoted_pairs(const std::string &s) {
std::string out;
out.reserve(s.size());
for (auto c : s) {
if (c == '\\' || c == '"') { out += '\\'; }
out += c;
}
return out;
}
inline bool parse_www_authenticate(const Response &res,
std::map<std::string, std::string> &auth,
bool is_proxy) {
@@ -10177,6 +10268,12 @@ inline bool is_field_valid(const std::string &name, const std::string &value) {
return is_field_name(name) && is_field_value(value);
}
// RFC 9112 §2.2/§3.2: the request-target has no SP, HTAB or other control
// characters (incl. bare CR). obs-text (raw UTF-8) is allowed.
inline bool is_request_target(const std::string &s) {
return std::all_of(s.begin(), s.end(), is_field_vchar);
}
} // namespace fields
inline bool perform_websocket_handshake(Stream &strm, Request &req,
@@ -10501,13 +10598,16 @@ inline bool process_server_socket_ssl(
socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec,
time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec,
time_t write_timeout_usec, T callback) {
// See process_server_socket(). The TLS library keeps already decrypted bytes
// of a pipelined request, which keep_alive() cannot see on the socket.
SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
write_timeout_sec, write_timeout_usec);
return process_server_socket_core(
svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec,
[&](bool close_connection, bool &connection_closed) {
SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
write_timeout_sec, write_timeout_usec);
// See the non-TLS path in process_server_socket().
strm.set_readable_hint();
[&]() { return strm.is_readable(); },
[&](bool socket_readable, bool close_connection,
bool &connection_closed) {
if (socket_readable) { strm.set_readable_hint(); }
return callback(strm, close_connection, connection_closed);
});
}
@@ -10548,7 +10648,13 @@ inline std::pair<std::string, std::string> make_digest_authentication_header(
}
std::string algo = "MD5";
if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); }
if (auth.find("algorithm") != auth.end()) {
// algorithm is an unquoted token (RFC 7616 §3.4). A server value that is
// not a token would otherwise be emitted verbatim and could carry commas
// or quotes that inject further auth-params into the header below.
const auto &a = auth.at("algorithm");
if (fields::is_token(a)) { algo = a; }
}
std::string response;
{
@@ -10571,14 +10677,23 @@ inline std::pair<std::string, std::string> make_digest_authentication_header(
auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("opaque") : "";
auto field = "Digest username=\"" + username + "\", realm=\"" +
auth.at("realm") + "\", nonce=\"" + auth.at("nonce") +
"\", uri=\"" + req.path + "\", algorithm=" + algo +
(qop.empty() ? ", response=\""
: ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" +
cnonce + "\", response=\"") +
response + "\"" +
(opaque.empty() ? "" : ", opaque=\"" + opaque + "\"");
// Every value placed inside a quoted-string is escaped so a '"' in it cannot
// close the string early. realm, nonce and opaque come straight from the
// server's challenge (parse_www_authenticate() already de-escaped them), so
// without this a crafted challenge injects extra auth-params into the header.
auto field =
"Digest username=\"" + detail::escape_quoted_pairs(username) +
"\", realm=\"" + detail::escape_quoted_pairs(auth.at("realm")) +
"\", nonce=\"" + detail::escape_quoted_pairs(auth.at("nonce")) +
"\", uri=\"" + detail::escape_quoted_pairs(req.path) +
"\", algorithm=" + algo +
(qop.empty() ? ", response=\""
: ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce +
"\", response=\"") +
response + "\"" +
(opaque.empty()
? ""
: ", opaque=\"" + detail::escape_quoted_pairs(opaque) + "\"");
auto key = is_proxy ? "Proxy-Authorization" : "Authorization";
return std::make_pair(key, field);
@@ -10643,11 +10758,10 @@ inline bool match_hostname(const std::string &pattern,
#ifdef _WIN32
// Verify certificate using Windows CertGetCertificateChain API.
// This provides real-time certificate validation with Windows Update
// integration, independent of the TLS backend (OpenSSL or MbedTLS).
inline bool
verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
const std::string &hostname,
bool verify_hostname, uint64_t &out_error) {
// integration, independent of the TLS backend.
inline bool verify_cert_with_windows_schannel(
const std::vector<unsigned char> &der_cert, const std::string &hostname,
bool verify_hostname, uint64_t &out_error, tls::const_session_t session) {
if (der_cert.empty()) { return false; }
out_error = 0;
@@ -10665,14 +10779,41 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
auto cert_guard =
scope_exit([&] { CertFreeCertificateContext(cert_context); });
// Give CryptoAPI the certificates the server sent. Without them it follows
// the leaf's AIA URL, which may lead to an issuer under an untrusted root.
std::vector<tls::cert_t> peer_certs;
tls::get_peer_certs(session, peer_certs);
auto store = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr);
auto store_guard = scope_exit([&] {
for (auto cert : peer_certs) {
tls::free_cert(cert);
}
if (store) { CertCloseStore(store, 0); }
});
for (auto cert : peer_certs) {
std::vector<unsigned char> der;
if (store && tls::get_cert_der(cert, der)) {
CertAddEncodedCertificateToStore(store, X509_ASN_ENCODING, der.data(),
static_cast<DWORD>(der.size()),
CERT_STORE_ADD_USE_EXISTING, nullptr);
}
}
// Setup chain parameters
CERT_CHAIN_PARA chain_para = {};
chain_para.cbSize = sizeof(chain_para);
// Require the server authentication usage along the chain, which also
// rejects roots that Windows trusts only for other purposes.
LPSTR server_auth = const_cast<LPSTR>(szOID_PKIX_KP_SERVER_AUTH);
chain_para.RequestedUsage.dwType = USAGE_MATCH_TYPE_AND;
chain_para.RequestedUsage.Usage.cUsageIdentifier = 1;
chain_para.RequestedUsage.Usage.rgpszUsageIdentifier = &server_auth;
// Build certificate chain with revocation checking
PCCERT_CHAIN_CONTEXT chain_context = nullptr;
auto chain_result = CertGetCertificateChain(
nullptr, cert_context, nullptr, cert_context->hCertStore, &chain_para,
nullptr, cert_context, nullptr, store, &chain_para,
CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
nullptr, &chain_context);
@@ -10780,6 +10921,9 @@ struct ClientTlsSessionOptions {
// The caller decides whether Schannel has anything to say about this
// connection; see SSLClient::initialize_ssl().
bool windows_cert_verification = false;
// A server certificate verifier works on the backend's chain verification,
// so the backend keeps deciding and Schannel only adds its own check.
bool server_certificate_verifier_set = false;
#endif
};
@@ -10814,12 +10958,24 @@ inline bool setup_client_tls_session(
return fail(Error::SSLConnection, 0, 0);
}
// With Windows verification on and no server certificate verifier set,
// Schannel is the only chain verifier. The backend's trust store is a
// snapshot of the Windows stores that lacks the roots Windows fetches on
// demand, so the backend's verdict is not used.
auto windows_verifies_chain = false;
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
windows_verifies_chain = options.windows_cert_verification &&
!options.server_certificate_verifier_set;
#endif
#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
// Mbed TLS and wolfSSL need the verification mode set explicitly; OpenSSL
// uses SSL_VERIFY_NONE and does all verification post-handshake. Chain
// verification happens during the handshake even for IP hosts; the
// certificate identity is verified post-handshake via verify_hostname().
set_verify_client(ctx, server_certificate_verification);
// uses SSL_VERIFY_NONE and does all verification post-handshake. Unless
// Schannel verifies the chain instead, chain verification happens during
// the handshake even for IP hosts; the certificate identity is verified
// post-handshake via verify_hostname().
set_verify_client(ctx,
server_certificate_verification && !windows_verifies_chain);
#endif
{
@@ -10864,10 +11020,12 @@ inline bool setup_client_tls_session(
if (verification_status == SSLVerifierResponse::NoDecisionMade &&
server_certificate_verification) {
auto verify_result = get_verify_result(session);
if (verify_result != 0) {
return fail(Error::SSLServerVerification, 0,
static_cast<uint64_t>(verify_result));
if (!windows_verifies_chain) {
auto verify_result = get_verify_result(session);
if (verify_result != 0) {
return fail(Error::SSLServerVerification, 0,
static_cast<uint64_t>(verify_result));
}
}
auto server_cert = get_peer_cert(session);
@@ -10887,18 +11045,17 @@ inline bool setup_client_tls_session(
}
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
// Additional Windows Schannel verification.
// This provides real-time certificate validation with Windows Update
// integration, working with both OpenSSL and MbedTLS backends.
// Windows Schannel verification, which lets Windows fetch missing roots
// and intermediates on demand. It must not be skipped: unless a server
// certificate verifier is set, it is the only chain check.
if (options.windows_cert_verification) {
std::vector<unsigned char> der;
if (get_cert_der(server_cert, der)) {
uint64_t wincrypt_error = 0;
if (!verify_cert_with_windows_schannel(
der, host, options.server_hostname_verification,
wincrypt_error)) {
return fail(Error::SSLServerVerification, 0, wincrypt_error);
}
uint64_t wincrypt_error = 0;
if (!get_cert_der(server_cert, der) ||
!verify_cert_with_windows_schannel(
der, host, options.server_hostname_verification, wincrypt_error,
session)) {
return fail(Error::SSLServerVerification, 0, wincrypt_error);
}
}
#endif
@@ -11660,6 +11817,7 @@ inline void Response::set_content_provider(
if (in_length > 0) { content_provider_ = std::move(provider); }
content_provider_resource_releaser_ = std::move(resource_releaser);
is_chunked_content_provider_ = false;
is_file_content_provider_ = false;
content_coding_ = detail::EncodingType::None;
}
@@ -11671,6 +11829,7 @@ inline void Response::set_content_provider(
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
content_provider_resource_releaser_ = std::move(resource_releaser);
is_chunked_content_provider_ = false;
is_file_content_provider_ = false;
content_coding_ = detail::EncodingType::None;
}
@@ -11682,6 +11841,7 @@ inline void Response::set_chunked_content_provider(
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
content_provider_resource_releaser_ = std::move(resource_releaser);
is_chunked_content_provider_ = true;
is_file_content_provider_ = false;
content_coding_ = detail::EncodingType::None;
}
@@ -12363,14 +12523,10 @@ inline bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out) {
struct in_addr v4;
if (inet_pton(AF_INET, addr_part.c_str(), &v4) == 1) {
int prefix = 32;
if (!prefix_part.empty()) {
auto r = from_chars(prefix_part.data(),
prefix_part.data() + prefix_part.size(), prefix);
if (r.ec != std::errc{} ||
r.ptr != prefix_part.data() + prefix_part.size()) {
return false;
}
if (prefix < 0 || prefix > 32) { return false; }
if (!prefix_part.empty() &&
!parse_int_in_range(prefix_part.data(), prefix_part.size(), 0, 32,
prefix)) {
return false;
}
out.kind = NoProxyKind::IPv4Cidr;
std::memcpy(out.net.data(), &v4, sizeof(v4));
@@ -12382,14 +12538,10 @@ inline bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out) {
struct in6_addr v6;
if (inet_pton(AF_INET6, addr_part.c_str(), &v6) == 1) {
int prefix = 128;
if (!prefix_part.empty()) {
auto r = from_chars(prefix_part.data(),
prefix_part.data() + prefix_part.size(), prefix);
if (r.ec != std::errc{} ||
r.ptr != prefix_part.data() + prefix_part.size()) {
return false;
}
if (prefix < 0 || prefix > 128) { return false; }
if (!prefix_part.empty() &&
!parse_int_in_range(prefix_part.data(), prefix_part.size(), 0, 128,
prefix)) {
return false;
}
out.kind = NoProxyKind::IPv6Cidr;
std::memcpy(out.net.data(), &v6, sizeof(v6));
@@ -13248,6 +13400,8 @@ inline bool Server::parse_request_line(const char *s, Request &req) const {
return false;
}
if (!detail::fields::is_request_target(req.target)) { return false; }
{
// Skip URL fragment
for (size_t i = 0; i < req.target.size(); i++) {
@@ -13335,9 +13489,27 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
if (!detail::write_response_line(bstrm, res.status)) { return false; }
if (header_writer_(bstrm, res.headers) <= 0) { return false; }
// Combine small body with headers to reduce write syscalls
if (req.method != "HEAD" && !res.body.empty() && !res.content_provider_) {
bstrm.write(res.body.data(), res.body.size());
// Combine a small body with the headers so the whole response leaves in a
// single write. A large body is written on its own instead: a copy of it
// costs more than the extra write saves.
auto send_body = req.method != "HEAD";
auto body_is_separate = false;
auto provider_done = false;
if (send_body && !res.body.empty() && !res.content_provider_) {
if (res.body.size() < CPPHTTPLIB_SEND_BUFSIZ) {
bstrm.write(res.body.data(), res.body.size());
} else {
body_is_separate = true;
}
} else if (send_body && res.content_provider_ &&
res.is_file_content_provider_ &&
res.content_length_ < CPPHTTPLIB_SEND_BUFSIZ) {
// A small file is read into the same buffer. Other providers may produce
// their data over time, so they are never held back.
if (!write_content_with_provider(bstrm, req, res, boundary, content_type)) {
return false;
}
provider_done = true;
}
// Log before writing to avoid race condition with client-side code that
@@ -13348,17 +13520,20 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
auto &data = bstrm.get_buffer();
if (!detail::write_data(strm, data.data(), data.size())) { return false; }
// Streaming body
auto ret = true;
if (req.method != "HEAD" && res.content_provider_) {
if (write_content_with_provider(strm, req, res, boundary, content_type)) {
res.content_provider_success_ = true;
} else {
ret = false;
}
if (body_is_separate) {
return detail::write_data(strm, res.body.data(), res.body.size());
}
return ret;
// Streaming body
if (send_body && res.content_provider_) {
if (!provider_done &&
!write_content_with_provider(strm, req, res, boundary, content_type)) {
return false;
}
res.content_provider_success_ = true;
}
return true;
}
inline bool
@@ -14289,6 +14464,13 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
// Connection has been closed on client
if (!line_reader.getline()) { return false; }
// RFC 9112 2.2: ignore an empty line received before the request-line. Some
// clients send an extra CRLF after a request body, which would otherwise be
// parsed as the next request on a persistent connection.
if (strcmp(line_reader.ptr(), "\r\n") == 0 && !line_reader.getline()) {
return false;
}
Request req;
req.start_time_ = std::chrono::steady_clock::now();
req.remote_addr = remote_addr;
@@ -14300,8 +14482,21 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
res.version = "HTTP/1.1";
res.headers = default_headers_;
// Request line and headers
// RFC 9112 §9.6: a server that sends the "close" connection option must
// close the connection after that response, whichever path wrote it (an
// error status, a handler, or a rejected request). Reading on would also
// parse whatever the client sent next on a connection it considers done.
auto honor_connection_close = detail::scope_exit([&] {
if (detail::has_header_token(res.headers, "Connection", "close")) {
connection_closed = true;
}
});
// Request line and headers. A rejected message leaves the rest of it (and
// any body) unread, so the connection cannot be reused: the leftover bytes
// would be parsed as the next request.
if (!parse_request_line(line_reader.ptr(), req)) {
connection_closed = true;
res.status = StatusCode::BadRequest_400;
output_error_log(Error::InvalidRequestLine, &req);
return write_response(strm, close_connection, req, res);
@@ -14309,20 +14504,28 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
// Request headers
if (!detail::read_headers(strm, req.headers)) {
connection_closed = true;
res.status = StatusCode::BadRequest_400;
output_error_log(Error::InvalidHeaders, &req);
return write_response(strm, close_connection, req, res);
}
// RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would
// otherwise let an intermediary and this parser disagree on where the body
// ends and enable request smuggling. Two cases: a non-zero Content-Length
// alongside any Transfer-Encoding (Content-Length: 0 is tolerated for
// compatibility with existing clients), and a Transfer-Encoding whose final
// coding is not chunked, which leaves the body length undeterminable. The
// latter must not fall through to the "no body" path, or the body bytes are
// parsed as the next request on a persistent connection.
if (detail::has_conflicting_content_length(req.headers) ||
// RFC 9112 §6.3: Reject requests whose framing is invalid or ambiguous,
// which would otherwise let an intermediary and this parser disagree on
// where the body ends and enable request smuggling. Three cases: a
// Content-Length that is not a valid decimal length (e.g. "42, 42", "+42"
// or empty), which would otherwise be read as "no body"; a non-zero
// Content-Length alongside any Transfer-Encoding (Content-Length: 0 is
// tolerated for compatibility with existing clients); and a
// Transfer-Encoding whose final coding is not chunked, which leaves the body
// length undeterminable. None of them may fall through to the "no body"
// path, or the body bytes are parsed as the next request on a persistent
// connection.
auto is_invalid_content_length = false;
detail::get_header_value_u64(req.headers, "Content-Length", 0, 0,
is_invalid_content_length);
if (is_invalid_content_length ||
detail::has_conflicting_content_length(req.headers) ||
(req.has_header("Transfer-Encoding") &&
!detail::is_chunked_transfer_encoding(req.headers))) {
connection_closed = true;
@@ -14595,17 +14798,13 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
// keep-alive. Without framing there is no body to drain — reading would
// consume the next request (issue #2450). If the response has committed the
// connection to close, there is no next request to protect.
if (!req.body_consumed_ && detail::has_framed_body(req)) {
if (detail::has_header_token(res.headers, "Connection", "close")) {
if (!req.body_consumed_ && detail::has_framed_body(req) &&
!detail::has_header_token(res.headers, "Connection", "close")) {
int dummy_status;
if (!detail::read_content(
strm, req, payload_max_length_, dummy_status, nullptr,
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
connection_closed = true;
} else {
int dummy_status;
if (!detail::read_content(
strm, req, payload_max_length_, dummy_status, nullptr,
[](const char *, size_t, size_t, size_t) { return true; },
false)) {
connection_closed = true;
}
}
}
@@ -15506,7 +15705,10 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
if (location.empty()) { return false; }
detail::UrlComponents uc;
if (!detail::parse_url(location, uc)) { return false; }
if (!detail::parse_url(detail::resolve_relative_location(location, req.path),
uc)) {
return false;
}
// Only follow http/https redirects
if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") {
@@ -18393,6 +18595,9 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
inline void
SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {
if (!ctx_) { return; }
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
server_certificate_verifier_set_ = static_cast<bool>(verifier);
#endif
tls::set_verify_callback(ctx_, verifier);
}
@@ -18454,6 +18659,9 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
enable_windows_cert_verification_ &&
system_ca_mode_ != SystemCAMode::Disabled && ca_cert_file_path_.empty() &&
ca_cert_dir_path_.empty() && ca_cert_pem_.empty() && !ca_cert_store_set_;
// Only a verifier set through set_server_certificate_verifier() is seen
// here, not one installed with tls::set_verify_callback() directly.
options.server_certificate_verifier_set = server_certificate_verifier_set_;
#endif
tls::session_t session = nullptr;
@@ -19423,6 +19631,24 @@ inline cert_t get_peer_cert(const_session_t session) {
static_cast<SSL *>(const_cast<void *>(session))));
}
inline size_t get_peer_certs(const_session_t session,
std::vector<cert_t> &certs) {
certs.clear();
if (!session) { return 0; }
auto ssl = static_cast<const SSL *>(session);
// On the server side, the chain leaves out the peer's own certificate
if (SSL_is_server(ssl)) {
if (auto leaf = get_peer_cert(session)) { certs.push_back(leaf); }
}
auto sk = SSL_get_peer_cert_chain(ssl);
for (int i = 0; sk && i < sk_X509_num(sk); i++) {
auto x509 = sk_X509_value(sk, i);
X509_up_ref(x509);
certs.push_back(static_cast<cert_t>(x509));
}
return certs.size();
}
inline void free_cert(cert_t cert) {
if (cert) { X509_free(static_cast<X509 *>(cert)); }
}
@@ -20767,6 +20993,18 @@ inline cert_t get_peer_cert(const_session_t session) {
return const_cast<mbedtls_x509_crt *>(cert);
}
inline size_t get_peer_certs(const_session_t session,
std::vector<cert_t> &certs) {
certs.clear();
// Mbed TLS parses the whole received chain into a list headed by the peer
// certificate, owned by the session like get_peer_cert()'s result
for (auto crt = static_cast<mbedtls_x509_crt *>(get_peer_cert(session));
crt && crt->raw.len > 0; crt = crt->next) {
certs.push_back(static_cast<cert_t>(crt));
}
return certs.size();
}
inline void free_cert(cert_t cert) {
// Mbed TLS: peer certificate is owned by the SSL context.
// No-op here, but callers should still call this for cross-backend
@@ -21924,6 +22162,24 @@ inline cert_t get_peer_cert(const_session_t session) {
return static_cast<cert_t>(cert);
}
inline size_t get_peer_certs(const_session_t session,
std::vector<cert_t> &certs) {
certs.clear();
if (!session) { return 0; }
// wolfSSL keeps the received chain only when built with SESSION_CERTS
#ifdef SESSION_CERTS
auto wsession =
static_cast<impl::WolfSSLSession *>(const_cast<void *>(session));
auto chain = wolfSSL_get_peer_chain(wsession->ssl);
auto count = chain ? wolfSSL_get_chain_count(chain) : 0;
for (int i = 0; i < count; i++) {
auto x509 = wolfSSL_get_chain_X509(chain, i);
if (x509) { certs.push_back(static_cast<cert_t>(x509)); }
}
#endif
return certs.size();
}
inline void free_cert(cert_t cert) {
if (cert) { wolfSSL_X509_free(static_cast<WOLFSSL_X509 *>(cert)); }
}
+805 -266
View File
File diff suppressed because it is too large Load Diff