Commit eec18f5d3 for llama.cpp
commit eec18f5d32099fb15d4ba15003a231bcc72757d5
Author: Alessandro de Oliveira Faria (A.K.A.CABELO) <cabelo@opensuse.org>
Date: Sat Oct 3 14:08:47 2026 -0300
vendor : update cpp-httplib to 0.59.0 (#29886)
diff --git a/scripts/sync_vendor.py b/scripts/sync_vendor.py
index 8ede79d57..bdfc8d62e 100755
--- a/scripts/sync_vendor.py
+++ b/scripts/sync_vendor.py
@@ -5,7 +5,7 @@ import os
import sys
import subprocess
-HTTPLIB_VERSION = "refs/tags/v0.58.0"
+HTTPLIB_VERSION = "refs/tags/v0.59.0"
# used by examples/gguf-hash, these repos have no release tag, so we pin a commit
XXHASH_COMMIT = "9f465f1ea932d6ad9a26cd77496311ffa544cd68"
diff --git a/vendor/cpp-httplib/httplib.cpp b/vendor/cpp-httplib/httplib.cpp
index 00c3ef7e1..781544763 100644
--- a/vendor/cpp-httplib/httplib.cpp
+++ b/vendor/cpp-httplib/httplib.cpp
@@ -169,30 +169,23 @@ void SSEClient::stop() {
}
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
@@ -200,14 +193,16 @@ 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);
@@ -274,6 +269,7 @@ 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());
@@ -287,18 +283,25 @@ 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;
}
}
@@ -343,11 +346,15 @@ bool SSEClient::should_reconnect(int count) const {
}
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;
}
}
@@ -408,6 +415,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();
@@ -1115,15 +1125,15 @@ 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);
@@ -1896,18 +1906,30 @@ bool keep_alive(const std::atomic<socket_t> &svr_sock, socket_t sock,
return false;
}
-template <typename T>
-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>
+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--;
}
@@ -1921,14 +1943,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);
});
}
@@ -2917,7 +2941,8 @@ 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;
}
@@ -3854,14 +3879,11 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
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 += ' ';
@@ -4083,9 +4105,9 @@ 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.
void set_file_content_provider(Response &res,
const std::shared_ptr<mmap> &m,
const std::string &content_type,
@@ -4097,6 +4119,7 @@ void set_file_content_provider(Response &res,
return true;
});
+ res.is_file_content_provider_ = true;
res.content_coding_ = encoding;
}
@@ -5388,6 +5411,19 @@ 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.
+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;
+}
+
bool parse_www_authenticate(const Response &res,
std::map<std::string, std::string> &auth,
bool is_proxy) {
@@ -5514,6 +5550,12 @@ 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.
+bool is_request_target(const std::string &s) {
+ return std::all_of(s.begin(), s.end(), is_field_vchar);
+}
+
} // namespace fields
bool perform_websocket_handshake(Stream &strm, Request &req,
@@ -5838,13 +5880,16 @@ 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);
});
}
@@ -5885,7 +5930,13 @@ 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;
{
@@ -5908,14 +5959,23 @@ 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);
@@ -5980,11 +6040,10 @@ 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).
-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.
+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;
@@ -6002,14 +6061,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);
@@ -6117,6 +6203,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
};
@@ -6151,12 +6240,24 @@ 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
{
@@ -6201,10 +6302,12 @@ 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);
@@ -6224,18 +6327,17 @@ 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
@@ -6997,6 +7099,7 @@ 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;
}
@@ -7008,6 +7111,7 @@ 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;
}
@@ -7019,6 +7123,7 @@ 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;
}
@@ -7700,14 +7805,10 @@ 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));
@@ -7719,14 +7820,10 @@ 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));
@@ -8585,6 +8682,8 @@ 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++) {
@@ -8672,9 +8771,27 @@ 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
@@ -8685,17 +8802,20 @@ 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; }
+ if (body_is_separate) {
+ return detail::write_data(strm, res.body.data(), res.body.size());
+ }
+
// 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 (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 ret;
+ return true;
}
bool
@@ -9626,6 +9746,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;
@@ -9637,8 +9764,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);
@@ -9646,20 +9786,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;
@@ -9932,17 +10080,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;
- }
}
}
@@ -10843,7 +10987,10 @@ 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") {
@@ -13730,6 +13877,9 @@ void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
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);
}
@@ -13791,6 +13941,9 @@ 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;
@@ -14760,6 +14913,24 @@ cert_t get_peer_cert(const_session_t session) {
static_cast<SSL *>(const_cast<void *>(session))));
}
+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();
+}
+
void free_cert(cert_t cert) {
if (cert) { X509_free(static_cast<X509 *>(cert)); }
}
@@ -16104,6 +16275,18 @@ cert_t get_peer_cert(const_session_t session) {
return const_cast<mbedtls_x509_crt *>(cert);
}
+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();
+}
+
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
@@ -17261,6 +17444,24 @@ cert_t get_peer_cert(const_session_t session) {
return static_cast<cert_t>(cert);
}
+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();
+}
+
void free_cert(cert_t cert) {
if (cert) { wolfSSL_X509_free(static_cast<WOLFSSL_X509 *>(cert)); }
}
diff --git a/vendor/cpp-httplib/httplib.h b/vendor/cpp-httplib/httplib.h
index 759d5dff8..283fa1387 100644
--- a/vendor/cpp-httplib/httplib.h
+++ b/vendor/cpp-httplib/httplib.h
@@ -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;