Commit 1ab23c33b for llama.cpp
commit 1ab23c33b7599e9e73fdcd6584985ae02ce8da83
Author: Alessandro de Oliveira Faria (A.K.A.CABELO) <cabelo@opensuse.org>
Date: Fri Oct 30 07:49:58 2026 -0300
vendor : update cpp-httplib to 0.64.0 (#31400)
diff --git a/scripts/sync_vendor.py b/scripts/sync_vendor.py
index 7b5876367..49af3f27c 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.62.0"
+HTTPLIB_VERSION = "refs/tags/v0.64.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 934d99054..ebe01d194 100644
--- a/vendor/cpp-httplib/httplib.cpp
+++ b/vendor/cpp-httplib/httplib.cpp
@@ -148,7 +148,8 @@ SSEClient &SSEClient::set_headers(const Headers &headers) {
bool SSEClient::is_connected() const { return connected_.load(); }
-const std::string &SSEClient::last_event_id() const {
+std::string SSEClient::last_event_id() const {
+ std::lock_guard<std::mutex> lock(last_event_id_mutex_);
return last_event_id_;
}
@@ -323,7 +324,10 @@ void SSEClient::run_event_loop() {
if (event_complete) {
// Update last_event_id for reconnection, even for an event that
// has no data. An empty id clears it.
- if (has_id) { last_event_id_ = current_msg.id; }
+ if (has_id) {
+ std::lock_guard<std::mutex> lock(last_event_id_mutex_);
+ last_event_id_ = current_msg.id;
+ }
// An event without a data field is not dispatched
if (has_data) { dispatch_event(current_msg); }
@@ -460,6 +464,8 @@ cert_t get_peer_cert(const_session_t session);
// 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);
+// Matches the host against the subjectAltName entries only, never the Common
+// Name (RFC 9525).
bool verify_hostname(cert_t cert, const char *hostname);
uint64_t hostname_mismatch_code();
long get_verify_result(const_session_t session);
@@ -929,32 +935,40 @@ bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
if (len < 126) {
header[1] = static_cast<uint8_t>(len);
if (mask) { header[1] |= 0x80; }
- if (strm.write(reinterpret_cast<char *>(header), 2) < 0) { return false; }
+ if (!write_data(strm, reinterpret_cast<char *>(header), 2)) {
+ return false;
+ }
} else if (len <= 0xFFFF) {
header[1] = 126;
if (mask) { header[1] |= 0x80; }
- if (strm.write(reinterpret_cast<char *>(header), 2) < 0) { return false; }
+ if (!write_data(strm, reinterpret_cast<char *>(header), 2)) {
+ return false;
+ }
uint8_t ext[2];
ext[0] = static_cast<uint8_t>((len >> 8) & 0xFF);
ext[1] = static_cast<uint8_t>(len & 0xFF);
- if (strm.write(reinterpret_cast<char *>(ext), 2) < 0) { return false; }
+ if (!write_data(strm, reinterpret_cast<char *>(ext), 2)) { return false; }
} else {
header[1] = 127;
if (mask) { header[1] |= 0x80; }
- if (strm.write(reinterpret_cast<char *>(header), 2) < 0) { return false; }
+ if (!write_data(strm, reinterpret_cast<char *>(header), 2)) {
+ return false;
+ }
uint8_t ext[8];
for (int i = 7; i >= 0; i--) {
ext[7 - i] =
static_cast<uint8_t>((static_cast<uint64_t>(len) >> (i * 8)) & 0xFF);
}
- if (strm.write(reinterpret_cast<char *>(ext), 8) < 0) { return false; }
+ if (!write_data(strm, reinterpret_cast<char *>(ext), 8)) { return false; }
}
if (mask) {
// Generate random mask key
uint8_t mask_key[4];
fill_random_bytes(mask_key, sizeof(mask_key));
- if (strm.write(reinterpret_cast<char *>(mask_key), 4) < 0) { return false; }
+ if (!write_data(strm, reinterpret_cast<char *>(mask_key), 4)) {
+ return false;
+ }
// Write masked payload in chunks
const size_t chunk_size = 4096;
@@ -965,11 +979,11 @@ bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
buf[i] =
data[offset + i] ^ static_cast<char>(mask_key[(offset + i) % 4]);
}
- if (strm.write(buf.data(), n) < 0) { return false; }
+ if (!write_data(strm, buf.data(), n)) { return false; }
}
} else {
if (len > 0) {
- if (strm.write(data, len) < 0) { return false; }
+ if (!write_data(strm, data, len)) { return false; }
}
}
@@ -1104,45 +1118,30 @@ bool is_valid_close_payload(const std::string &payload) {
namespace detail {
+// A path a mount point serves: absolute, with no empty segment except a
+// trailing '/', no segment ending in '.' or a space (which covers "." and
+// ".." and names Windows trims into another file's name) and, on Windows, no
+// ':' (which names an NTFS stream, so "a.txt::$DATA" is a.txt), so the file
+// served is the one that req.path names to the handlers that ran before it.
bool is_valid_path(const std::string &path) {
- size_t level = 0;
- size_t i = 0;
-
- // Skip slash
- while (i < path.size() && path[i] == '/') {
- i++;
- }
+ if (path.empty() || path[0] != '/') { return false; }
- while (i < path.size()) {
- // Read component
- auto beg = i;
- while (i < path.size() && path[i] != '/') {
- if (path[i] == '\0') {
- return false;
- } else if (path[i] == '\\') {
- return false;
- }
- i++;
+ auto beg = size_t{1};
+ for (auto i = beg; i <= path.size(); i++) {
+ if (i < path.size() && path[i] != '/') {
+ if (path[i] == '\0' || path[i] == '\\') { return false; }
+#ifdef _WIN32
+ if (path[i] == ':') { return false; }
+#endif
+ continue;
}
-
auto len = i - beg;
- assert(len > 0);
-
- if (!path.compare(beg, len, ".")) {
- ;
- } else if (!path.compare(beg, len, "..")) {
- if (level == 0) { return false; }
- level--;
- } else {
- level++;
- }
-
- // Skip slash
- while (i < path.size() && path[i] == '/') {
- i++;
+ if ((len == 0 && i < path.size()) ||
+ (len > 0 && (path[i - 1] == '.' || path[i - 1] == ' '))) {
+ return false;
}
+ beg = i + 1;
}
-
return true;
}
@@ -2890,13 +2889,10 @@ std::string
find_content_type(const std::string &path,
const std::map<std::string, std::string> &user_data,
const std::string &default_content_type) {
- auto ext = file_extension(path);
+ // Extensions are matched regardless of case, as web servers do
+ auto ext = case_ignore::to_lower(file_extension(path));
- // Extensions are matched regardless of case, as web servers do, with an
- // exact match to a user mapping first
- auto lower_ext = case_ignore::to_lower(ext);
auto it = user_data.find(ext);
- if (it == user_data.end()) { it = user_data.find(lower_ext); }
if (it != user_data.end()) { return it->second; }
static const struct {
@@ -2955,7 +2951,7 @@ find_content_type(const std::string &path,
};
for (const auto &t : types) {
- if (lower_ext == t.ext) { return t.type; }
+ if (ext == t.ext) { return t.type; }
}
return default_content_type;
}
@@ -3580,6 +3576,18 @@ void set_header(Headers &headers, const std::string &key,
if (fields::is_field_valid(key, val)) { headers.emplace(key, val); }
}
+// Drops the fields set_header() would refuse, from headers an application can
+// fill directly: CR/LF in one would split the message.
+void erase_invalid_fields(Headers &headers) {
+ for (auto it = headers.begin(); it != headers.end();) {
+ if (fields::is_field_valid(it->first, it->second)) {
+ ++it;
+ } else {
+ it = headers.erase(it);
+ }
+ }
+}
+
bool read_headers(Stream &strm, Headers &headers) {
const auto bufsiz = 2048;
char buf[bufsiz];
@@ -4057,13 +4065,6 @@ ssize_t write_response_line(Stream &strm, int status) {
ssize_t write_headers(Stream &strm, const Headers &headers) {
ssize_t write_len = 0;
for (const auto &x : headers) {
- // Skip fields with invalid names or values to prevent response splitting
- // via CR/LF injection, matching set_header(). The client validates request
- // headers up front in check_and_write_headers, but the server passes
- // res.headers straight to this writer, and res.headers is a public field
- // an application can populate directly with request-derived values.
- if (!fields::is_field_valid(x.first, x.second)) { continue; }
-
std::string s;
s = x.first;
s += ": ";
@@ -5539,15 +5540,6 @@ bool has_framed_body(const Request &req) {
req.get_header_value_u64("Content-Length") > 0;
}
-bool is_connection_persistent(const Request &req) {
- if (has_header_token(req.headers, "Connection", "close")) { return false; }
- if (req.version == "HTTP/1.0" &&
- !has_header_token(req.headers, "Connection", "keep-alive")) {
- return false;
- }
- return true;
-}
-
bool expect_content(const Request &req) {
if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
req.method == "DELETE") {
@@ -5811,6 +5803,32 @@ bool is_ip_address(const std::string &host) {
inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
}
+// One spelling for an IPv6 address, and the IPv4 one for an IPv4-mapped IPv6
+// address, which is how a dual-stack socket reports an IPv4 peer. Anything
+// else, including an address with a zone ID, is returned as it is.
+std::string canonical_ip(const std::string &addr) {
+ struct in6_addr addr6;
+ if (addr.find('%') != std::string::npos ||
+ inet_pton(AF_INET6, addr.c_str(), &addr6) != 1) {
+ return addr;
+ }
+
+ char buf[INET6_ADDRSTRLEN];
+ if (IN6_IS_ADDR_V4MAPPED(&addr6)) {
+ return inet_ntop(AF_INET, &addr6.s6_addr[12], buf, sizeof(buf)) ? buf
+ : addr;
+ }
+ return inet_ntop(AF_INET6, &addr6, buf, sizeof(buf)) ? buf : addr;
+}
+
+// `trusted_proxies` holds canonical_ip() spellings.
+bool is_trusted_proxy(const std::string &addr,
+ const std::vector<std::string> &trusted_proxies) {
+ if (trusted_proxies.empty()) { return false; }
+ return std::find(trusted_proxies.begin(), trusted_proxies.end(),
+ canonical_ip(addr)) != trusted_proxies.end();
+}
+
// Resolve where a client should connect for `host`, honoring a user-supplied
// hostname-to-address map. `host` itself is never rewritten, so it keeps
// supplying the Host header and SNI; only the connection target changes.
@@ -6492,6 +6510,17 @@ bool setup_client_tls_session(
return false;
};
+ // A TLS failure, whose verification codes name the Error too
+ auto fail_tls = [&](ErrorCode code, uint64_t backend_error) {
+ auto error = Error::SSLConnection;
+ if (code == ErrorCode::CertVerifyFailed) {
+ error = Error::SSLServerVerification;
+ } else if (code == ErrorCode::HostnameMismatch) {
+ error = Error::SSLServerHostnameVerification;
+ }
+ return fail(error, static_cast<int>(code), backend_error);
+ };
+
if (!ctx) {
session = nullptr;
return fail(Error::SSLConnection, 0, 0);
@@ -6507,31 +6536,34 @@ bool setup_client_tls_session(
!options.server_certificate_verifier_set;
#endif
+ // The chain, and a host name, are verified during the handshake, so that a
+ // server failing them never gets to see a client certificate. Unless
+ // Schannel verifies the chain instead, this holds for IP hosts too, whose
+ // identity is checked after the handshake by verify_hostname().
+ auto verify_in_handshake =
+ server_certificate_verification && !windows_verifies_chain;
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+ // A session verifier may accept a certificate the built-in check rejects,
+ // and runs only after the handshake
+ verify_in_handshake = verify_in_handshake && !options.session_verifier;
+#endif
+
{
std::unique_lock<std::mutex> guard;
if (options.ctx_mutex) {
guard = std::unique_lock<std::mutex>(*options.ctx_mutex);
}
-#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
- // Mbed TLS and wolfSSL need the verification mode set explicitly, on the
- // context, so under the same lock; OpenSSL 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
+ // The mode is set on the context, so under the same lock
+ set_verify_client(ctx, verify_in_handshake);
session = create_session(ctx, sock);
}
if (!session) { return fail(Error::SSLConnection, 0, get_error()); }
// RFC 6066: SNI must not be set for IP addresses; skip it for IP hosts, so
- // their identity is checked post-handshake below instead. On Mbed TLS and
- // wolfSSL, set_sni also drives handshake-time hostname verification, so
- // options.server_hostname_verification is threaded through here.
+ // their identity is checked post-handshake below instead.
if (!is_ip_address(host)) {
- if (!set_sni(session, host.c_str(), options.server_hostname_verification)) {
+ if (!set_sni(session, host.c_str(),
+ verify_in_handshake && options.server_hostname_verification)) {
return fail(Error::SSLConnection, 0, get_error());
}
}
@@ -6539,13 +6571,7 @@ bool setup_client_tls_session(
TlsError tls_err;
if (!connect_nonblocking(session, sock, timeout_sec, timeout_usec,
&tls_err)) {
- auto error = Error::SSLConnection;
- if (tls_err.code == ErrorCode::CertVerifyFailed) {
- error = Error::SSLServerVerification;
- } else if (tls_err.code == ErrorCode::HostnameMismatch) {
- error = Error::SSLServerHostnameVerification;
- }
- return fail(error, static_cast<int>(tls_err.code), tls_err.backend_code);
+ return fail_tls(tls_err.code, tls_err.backend_code);
}
auto verification_status = SSLVerifierResponse::NoDecisionMade;
@@ -6562,14 +6588,14 @@ bool setup_client_tls_session(
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));
+ return fail_tls(ErrorCode::CertVerifyFailed,
+ static_cast<uint64_t>(verify_result));
}
}
auto server_cert = get_peer_cert(session);
if (!server_cert) {
- return fail(Error::SSLServerVerification, 0, get_error());
+ return fail_tls(ErrorCode::CertVerifyFailed, get_error());
}
auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); });
@@ -6578,8 +6604,7 @@ bool setup_client_tls_session(
// hostname is bound during the handshake.
if (options.server_hostname_verification) {
if (!verify_hostname(server_cert, host.c_str())) {
- return fail(Error::SSLServerHostnameVerification, 0,
- hostname_mismatch_code());
+ return fail_tls(ErrorCode::HostnameMismatch, hostname_mismatch_code());
}
}
@@ -6594,7 +6619,7 @@ bool setup_client_tls_session(
!verify_cert_with_windows_schannel(
der, host, options.server_hostname_verification, wincrypt_error,
session)) {
- return fail(Error::SSLServerVerification, 0, wincrypt_error);
+ return fail_tls(ErrorCode::CertVerifyFailed, wincrypt_error);
}
}
#endif
@@ -7056,8 +7081,13 @@ std::string decode_query_component(const std::string &component,
}
std::string sanitize_filename(const std::string &filename) {
- // Extract basename: find the last path separator (/ or \)
+ // Extract basename: find the last path separator (/ or \) and, on Windows,
+ // ':', which ends a drive prefix ("C:..") or names an NTFS stream ("a:b")
+#ifdef _WIN32
+ auto pos = filename.find_last_of("/\\:");
+#else
auto pos = filename.find_last_of("/\\");
+#endif
auto result =
(pos != std::string::npos) ? filename.substr(pos + 1) : filename;
@@ -8122,6 +8152,17 @@ make_host_and_port_string_always_port(const std::string &host, int port) {
return prepare_host_string(host) + ":" + std::to_string(port);
}
+// Request target of a CONNECT for `host`, with `addr_map` applied.
+std::string
+make_connect_request_target(const std::map<std::string, std::string> &addr_map,
+ const std::string &host, int port) {
+ std::string connect_host;
+ std::string ip;
+ apply_addr_map(addr_map, host, connect_host, ip);
+ return make_host_and_port_string_always_port(ip.empty() ? connect_host : ip,
+ port);
+}
+
// Value for the Host header a client sends when the caller supplied none.
// Only the value: callers decide where in their header list it goes.
std::string make_default_host_header_value(const std::string &host,
@@ -8849,7 +8890,10 @@ bool Server::remove_mount_point(const std::string &mount_point) {
Server &
Server::set_file_extension_and_mimetype_mapping(const std::string &ext,
const std::string &mime) {
- file_extension_and_mimetype_map_[ext] = mime;
+ // Keyed as find_content_type() looks it up
+ auto key = detail::case_ignore::to_lower(ext);
+ if (!key.empty() && key[0] == '.') { key.erase(0, 1); }
+ file_extension_and_mimetype_map_[key] = mime;
return *this;
}
@@ -8957,7 +9001,11 @@ Server &Server::set_header_writer(
Server &
Server::set_trusted_proxies(const std::vector<std::string> &proxies) {
- trusted_proxies_ = proxies;
+ trusted_proxies_.clear();
+ trusted_proxies_.reserve(proxies.size());
+ for (const auto &proxy : proxies) {
+ trusted_proxies_.push_back(detail::canonical_ip(proxy));
+ }
return *this;
}
@@ -9003,6 +9051,11 @@ Server &Server::set_payload_max_length(size_t length) {
return *this;
}
+Server &Server::set_compress(bool on) {
+ compress_ = on;
+ return *this;
+}
+
Server &Server::set_static_file_compression(bool on) {
static_file_compression_ = on;
return *this;
@@ -9158,6 +9211,10 @@ bool Server::write_response_core(Stream &strm, bool close_connection,
need_apply_ranges = true;
}
+ // Before anything below reads res.headers, which the handlers may have
+ // filled directly
+ detail::erase_invalid_fields(res.headers);
+
if (!detail::body_allowed_for_status(res.status)) {
detail::clear_content(res);
}
@@ -9201,6 +9258,7 @@ bool Server::write_response_core(Stream &strm, bool close_connection,
if (post_routing_handler_) {
post_routing_handler_(req, res);
+ detail::erase_invalid_fields(res.headers);
// Again, for a body or framing headers the handler changed.
detail::set_framing_headers(req, res, boundary, content_type);
}
@@ -9413,38 +9471,10 @@ bool Server::read_content_core(
}
// RFC 9112 §6: no Transfer-Encoding and no Content-Length means no body.
- // For non-SSL builds we still scan non-persistent connections for stray
- // body bytes so the payload limit is enforced (413). On keep-alive,
- // pending bytes may be the next request (issue #2450), so skip.
-#if !defined(CPPHTTPLIB_SSL_ENABLED)
- if (!req.has_header("Content-Length") &&
- !detail::is_chunked_transfer_encoding(req.headers)) {
- if (!detail::is_connection_persistent(req) && payload_max_length_ > 0 &&
- payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
- auto has_data = strm.is_readable();
- if (!has_data) {
- auto s = strm.socket();
- if (s != INVALID_SOCKET) {
- has_data = detail::select_read(s, 0, 0) > 0;
- }
- }
- if (has_data) {
- // Route through the same decompressing reader used by the
- // length-framed and chunked paths below, so payload_max_length_ is
- // enforced on the decompressed size here too instead of only on the
- // compressed wire bytes.
- return detail::read_content(strm, req, payload_max_length_, res.status,
- nullptr, out, true);
- }
- }
- return true;
- }
-#else
if (!req.has_header("Content-Length") &&
!detail::is_chunked_transfer_encoding(req.headers)) {
return true;
}
-#endif
// A second read would start where the first one stopped, or in the next
// request.
@@ -9479,6 +9509,8 @@ bool Server::read_content_core(
}
bool Server::handle_file_request(Request &req, Response &res) {
+ if (!detail::is_valid_path(req.path)) { return false; }
+
for (const auto &entry : base_dirs_) {
// Prefix match, on a path segment boundary. A mount point of "/mount"
// covers "/mount" and "/mount/...", but must not swallow "/mountdir/...".
@@ -9489,85 +9521,80 @@ bool Server::handle_file_request(Request &req, Response &res) {
req.path.size() == entry.mount_point.size() ||
req.path[entry.mount_point.size()] == '/')) {
std::string sub_path = "/" + req.path.substr(entry.mount_point.size());
- if (detail::is_valid_path(sub_path)) {
- auto path = entry.base_dir + sub_path;
- if (path.back() == '/') { path += "index.html"; }
-
- // Defense-in-depth: is_valid_path blocks ".." traversal in the URL,
- // but symlinks/junctions can still escape the base directory. A path
- // that cannot be resolved, which includes one that does not exist, is
- // not served from here.
- std::string resolved_path;
- if (!detail::canonicalize_path(path.c_str(), resolved_path)) {
- continue;
- }
- if (!detail::is_path_within_base(resolved_path,
- entry.resolved_base_dir)) {
- res.status = StatusCode::Forbidden_403;
- return true;
- }
-
- detail::FileStat stat(path);
+ auto path = entry.base_dir + sub_path;
+ if (path.back() == '/') { path += "index.html"; }
+
+ // Defense-in-depth: is_valid_path blocks ".." traversal in the URL,
+ // but symlinks/junctions can still escape the base directory. A path
+ // that cannot be resolved, which includes one that does not exist, is
+ // not served from here.
+ std::string resolved_path;
+ if (!detail::canonicalize_path(path.c_str(), resolved_path)) { continue; }
+ if (!detail::is_path_within_base(resolved_path,
+ entry.resolved_base_dir)) {
+ res.status = StatusCode::Forbidden_403;
+ return true;
+ }
- if (stat.is_dir()) {
- // Relative, so "//dir" cannot name another host; "./" keeps a ':'
- // in the name from reading as a scheme.
- auto name = req.path.substr(req.path.rfind('/') + 1);
- res.set_redirect("./" + encode_path_component(name) + "/",
- StatusCode::MovedPermanently_301);
- return true;
- }
+ detail::FileStat stat(path);
- if (stat.is_file()) {
- for (const auto &kv : entry.headers) {
- res.set_header(kv.first, kv.second);
- }
+ if (stat.is_dir()) {
+ // Relative, so "//dir" cannot name another host; "./" keeps a ':'
+ // in the name from reading as a scheme.
+ auto name = req.path.substr(req.path.rfind('/') + 1);
+ res.set_redirect("./" + encode_path_component(name) + "/",
+ StatusCode::MovedPermanently_301);
+ return true;
+ }
- auto content_type = detail::find_content_type(
- path, file_extension_and_mimetype_map_, default_file_mimetype_);
- auto encoding = detail::EncodingType::None;
- if (static_file_compression_) {
- encoding =
- static_file_encoding(req, res, content_type, stat.size());
- }
+ if (stat.is_file()) {
+ for (const auto &kv : entry.headers) {
+ res.set_header(kv.first, kv.second);
+ }
- // The ETag names the representation actually sent, so a client that
- // cached the compressed form revalidates against the compressed ETag
- // and still gets a 304, while one that took identity keeps the plain
- // ETag.
- auto etag = detail::compute_etag(
- stat, encoding == detail::EncodingType::None
- ? std::string()
- : std::string("-") + detail::encoding_name(encoding));
- if (!etag.empty()) { res.set_header("ETag", etag); }
-
- auto mtime = stat.mtime();
-
- auto last_modified = detail::file_mtime_to_http_date(mtime);
- if (!last_modified.empty()) {
- res.set_header("Last-Modified", last_modified);
- }
+ auto content_type = detail::find_content_type(
+ path, file_extension_and_mimetype_map_, default_file_mimetype_);
+ auto encoding = detail::EncodingType::None;
+ if (static_file_compression_) {
+ encoding = static_file_encoding(req, res, content_type, stat.size());
+ }
- if (check_if_not_modified(req, res, etag, mtime)) { return true; }
+ // The ETag names the representation actually sent, so a client that
+ // cached the compressed form revalidates against the compressed ETag
+ // and still gets a 304, while one that took identity keeps the plain
+ // ETag.
+ auto etag = detail::compute_etag(
+ stat, encoding == detail::EncodingType::None
+ ? std::string()
+ : std::string("-") + detail::encoding_name(encoding));
+ if (!etag.empty()) { res.set_header("ETag", etag); }
+
+ auto mtime = stat.mtime();
+
+ auto last_modified = detail::file_mtime_to_http_date(mtime);
+ if (!last_modified.empty()) {
+ res.set_header("Last-Modified", last_modified);
+ }
- check_if_range(req, etag, mtime);
+ if (check_if_not_modified(req, res, etag, mtime)) { return true; }
- auto mm = std::make_shared<detail::mmap>(path.c_str());
- if (!mm->is_open()) {
- output_error_log(Error::OpenFile, &req);
- return false;
- }
+ check_if_range(req, etag, mtime);
- detail::set_file_content_provider(res, mm, content_type, encoding);
+ auto mm = std::make_shared<detail::mmap>(path.c_str());
+ if (!mm->is_open()) {
+ output_error_log(Error::OpenFile, &req);
+ return false;
+ }
- if (req.method != "HEAD" && file_request_handler_) {
- file_request_handler_(req, res);
- }
+ detail::set_file_content_provider(res, mm, content_type, encoding);
- return true;
- } else {
- output_error_log(Error::OpenFile, &req);
+ if (req.method != "HEAD" && file_request_handler_) {
+ file_request_handler_(req, res);
}
+
+ return true;
+ } else {
+ output_error_log(Error::OpenFile, &req);
}
}
}
@@ -9794,6 +9821,17 @@ bool Server::listen_internal() {
return ret;
}
+#ifndef CPPHTTPLIB_NO_EXCEPTIONS
+// Call from a catch block. The response is a 500 unless the exception handler
+// sets another status. Returns whether the exception handler ran.
+bool Server::handle_exception(Request &req, Response &res) {
+ res.status = StatusCode::InternalServerError_500;
+ if (!exception_handler_) { return false; }
+ exception_handler_(req, res, std::current_exception());
+ return true;
+}
+#endif
+
bool Server::routing(Request &req, Response &res, Stream &strm,
bool skip_pre_routing) {
if (!skip_pre_routing && pre_routing_handler_ &&
@@ -10029,7 +10067,8 @@ void Server::apply_ranges(const Request &req, Response &res,
"multipart/byteranges; boundary=" + boundary);
}
- auto type = detail::encoding_type(req, res);
+ auto type =
+ compress_ ? detail::encoding_type(req, res) : detail::EncodingType::None;
if (res.body.empty()) {
if (res.content_length_ > 0 && req.ranges.size() == 1 &&
@@ -10136,12 +10175,7 @@ get_client_ip(const std::string &x_forwarded_for,
// returned as the client.
for (size_t i = ip_list.size(); i-- > 0;) {
const auto &ip = ip_list[i];
-
- auto is_trusted_proxy =
- std::any_of(trusted_proxies.begin(), trusted_proxies.end(),
- [&](const std::string &proxy) { return ip == proxy; });
-
- if (!is_trusted_proxy) { return ip; }
+ if (!detail::is_trusted_proxy(ip, trusted_proxies)) { return ip; }
}
// Every hop was a trusted proxy; fall back to the first entry.
@@ -10161,7 +10195,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
// The read timeout bounds the request line and headers as a whole, so a
// client sending them a byte at a time cannot hold the worker any longer.
- strm.set_max_timeout(read_timeout_sec_ * 1000 + read_timeout_usec_ / 1000);
+ auto read_timeout_msec = read_timeout_sec_ * 1000 + read_timeout_usec_ / 1000;
+ strm.set_max_timeout(read_timeout_msec);
// Connection has been closed on client
if (!line_reader.getline()) { return false; }
@@ -10236,6 +10271,16 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
return write_response(strm, close_connection, req, res);
}
+ // RFC 9112 6.1: a lone "chunked" is the only transfer coding implemented
+ if (req.has_header("Transfer-Encoding") &&
+ (req.get_header_value_count("Transfer-Encoding") != 1 ||
+ !detail::case_ignore::equal(req.get_header_value("Transfer-Encoding"),
+ "chunked"))) {
+ connection_closed = true;
+ res.status = StatusCode::NotImplemented_501;
+ return write_response(strm, close_connection, req, res);
+ }
+
// RFC 9112 §6.1: a Transfer-Encoding alongside a Content-Length, or in an
// HTTP/1.0 request, is honoured, but the connection closes after the
// response.
@@ -10273,11 +10318,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
// Only honor X-Forwarded-For if the peer on the actual TCP connection is
// itself a trusted proxy. Otherwise any direct client could spoof
// remote_addr simply by sending an arbitrary X-Forwarded-For header.
- auto is_trusted_peer = std::any_of(
- trusted_proxies_.begin(), trusted_proxies_.end(),
- [&](const std::string &proxy) { return proxy == remote_addr; });
-
- if (is_trusted_peer && req.has_header("X-Forwarded-For")) {
+ if (detail::is_trusted_proxy(remote_addr, trusted_proxies_) &&
+ req.has_header("X-Forwarded-For")) {
// Some proxies append the address they observed as a separate
// X-Forwarded-For field line instead of extending the one the client sent
// (e.g. HAProxy's "option forwardfor"), so the whole combined value has to
@@ -10306,7 +10348,9 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
}
}
- if (req.has_header("Range")) {
+ // RFC 9110 14.2: range handling is defined only for GET (and so for HEAD)
+ if ((req.method == "GET" || req.method == "HEAD") &&
+ req.has_header("Range")) {
const auto &range_header_value = req.get_header_value("Range");
if (!detail::parse_range_header(range_header_value, req.ranges)) {
connection_closed = true;
@@ -10357,28 +10401,27 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
// An upgrade request has been through pre-routing before it reaches routing()
auto is_upgrade = detail::is_websocket_upgrade(req);
if (is_upgrade) {
- if (pre_routing_handler_ &&
- pre_routing_handler_(req, res) == HandlerResponse::Handled) {
- if (res.status == -1) { res.status = StatusCode::OK_200; }
- return write_response_with_content(strm, close_after_response, req, res);
- }
- // Find matching WebSocket handler
- for (const auto &entry : websocket_handlers_) {
- if (entry.matcher->match(req)) {
+ // A response given here leaves the body unread, and it would be parsed as
+ // the next request.
+ auto close_if_answered =
+ close_after_response || detail::has_framed_body(req);
+
+ const WebSocketHandlerEntry *ws_route = nullptr;
+ std::string selected_subprotocol;
+ auto answer_before_upgrade = [&]() -> bool {
+ if (pre_routing_handler_ &&
+ pre_routing_handler_(req, res) == HandlerResponse::Handled) {
+ return true;
+ }
+ for (const auto &entry : websocket_handlers_) {
+ if (!entry.matcher->match(req)) { continue; }
req.matched_route = entry.matcher->pattern();
if (pre_request_handler_ &&
pre_request_handler_(req, res) == HandlerResponse::Handled) {
- if (res.status == -1) { res.status = StatusCode::OK_200; }
- return write_response_with_content(strm, close_after_response, req,
- res);
+ return true;
}
+ ws_route = &entry;
- // Compute accept key
- auto client_key = req.get_header_value("Sec-WebSocket-Key");
- auto accept_key = detail::websocket_accept_key(client_key);
-
- // Negotiate subprotocol
- std::string selected_subprotocol;
if (entry.sub_protocol_selector) {
auto protocol_header = detail::get_combined_header_value(
req.headers, "Sec-WebSocket-Protocol");
@@ -10398,57 +10441,78 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
}
}
}
+ return false;
+ }
+ return false;
+ };
- // Send 101 Switching Protocols
- std::string handshake_response = "HTTP/1.1 101 Switching Protocols\r\n"
- "Upgrade: websocket\r\n"
- "Connection: Upgrade\r\n"
- "Sec-WebSocket-Accept: " +
- accept_key + "\r\n";
- if (!selected_subprotocol.empty()) {
- if (!detail::fields::is_field_value(selected_subprotocol)) {
- return false;
- }
- handshake_response +=
- "Sec-WebSocket-Protocol: " + selected_subprotocol + "\r\n";
- }
- handshake_response += "\r\n";
- if (strm.write(handshake_response.data(), handshake_response.size()) <
- 0) {
+ auto answered = false;
+#ifdef CPPHTTPLIB_NO_EXCEPTIONS
+ answered = answer_before_upgrade();
+#else
+ try {
+ answered = answer_before_upgrade();
+ } catch (...) {
+ if (!handle_exception(req, res)) {
+ return write_response(strm, close_if_answered, req, res);
+ }
+ answered = true;
+ }
+#endif
+ if (answered) {
+ if (res.status == -1) { res.status = StatusCode::OK_200; }
+ return write_response_with_content(strm, close_if_answered, req, res);
+ }
+
+ if (ws_route) {
+ auto client_key = req.get_header_value("Sec-WebSocket-Key");
+ auto accept_key = detail::websocket_accept_key(client_key);
+
+ // Send 101 Switching Protocols
+ std::string handshake_response = "HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: " +
+ accept_key + "\r\n";
+ if (!selected_subprotocol.empty()) {
+ if (!detail::fields::is_field_value(selected_subprotocol)) {
return false;
}
+ handshake_response +=
+ "Sec-WebSocket-Protocol: " + selected_subprotocol + "\r\n";
+ }
+ handshake_response += "\r\n";
+ if (strm.write(handshake_response.data(), handshake_response.size()) <
+ 0) {
+ return false;
+ }
- connection_closed = true;
- if (websocket_upgraded) { *websocket_upgraded = true; }
+ connection_closed = true;
+ if (websocket_upgraded) { *websocket_upgraded = true; }
- {
#ifdef CPPHTTPLIB_SSL_ENABLED
- if (req.ssl) {
- // wss: the heartbeat ping thread and the read path enter the same
- // TLS session from different threads. Hand the WebSocket a stream
- // that serializes every TLS call, so the shared SSLSocketStream on
- // the plain HTTP/HTTPS paths stays untouched.
- auto ws_strm =
- std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
- strm.socket(), const_cast<tls::session_t>(req.ssl),
- CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0,
- write_timeout_sec_, write_timeout_usec_));
- ws::WebSocket ws(std::move(ws_strm), req, true,
- websocket_ping_interval_sec_,
- websocket_max_missed_pongs_);
- entry.handler(req, ws);
- return true;
- }
-#endif
- // Use WebSocket-specific read timeout instead of HTTP timeout
- strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND,
- 0);
- ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_,
- websocket_max_missed_pongs_);
- entry.handler(req, ws);
- }
+ if (req.ssl) {
+ // wss: the heartbeat ping thread and the read path enter the same
+ // TLS session from different threads. Hand the WebSocket a stream
+ // that serializes every TLS call, so the shared SSLSocketStream on
+ // the plain HTTP/HTTPS paths stays untouched.
+ auto ws_strm = std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
+ strm.socket(), const_cast<tls::session_t>(req.ssl),
+ CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0,
+ write_timeout_sec_, write_timeout_usec_));
+ ws::WebSocket ws(std::move(ws_strm), req, true,
+ websocket_ping_interval_sec_,
+ websocket_max_missed_pongs_);
+ ws_route->handler(req, ws);
return true;
}
+#endif
+ // Use WebSocket-specific read timeout instead of HTTP timeout
+ strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0);
+ ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_,
+ websocket_max_missed_pongs_);
+ ws_route->handler(req, ws);
+ return true;
}
// No WebSocket route: route it as a plain HTTP request
}
@@ -10460,23 +10524,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
#else
try {
routed = routing(req, res, strm, is_upgrade);
- } catch (std::exception &) {
- if (exception_handler_) {
- auto ep = std::current_exception();
- exception_handler_(req, res, ep);
- routed = true;
- } else {
- res.status = StatusCode::InternalServerError_500;
- }
- } catch (...) {
- if (exception_handler_) {
- auto ep = std::current_exception();
- exception_handler_(req, res, ep);
- routed = true;
- } else {
- res.status = StatusCode::InternalServerError_500;
- }
- }
+ } catch (...) { routed = handle_exception(req, res); }
#endif
auto ret = false;
if (routed) {
@@ -10532,14 +10580,18 @@ 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. A body read in
- // part cannot be drained from where it stopped, so the connection closes.
+ // part cannot be drained from where it stopped, so the connection closes,
+ // as it does when the body is longer than the drain limit or does not
+ // arrive within the read timeout.
if (!req.body_consumed_ && detail::has_framed_body(req) &&
!detail::has_header_token(res.headers, "Connection", "close")) {
+ strm.set_max_timeout(read_timeout_msec);
int dummy_status;
if (req.body_read_started_ ||
!detail::read_content(
- strm, req, payload_max_length_, dummy_status, nullptr,
- [](const char *, size_t, size_t, size_t) { return true; }, false)) {
+ strm, req, CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH, dummy_status,
+ nullptr, [](const char *, size_t, size_t, size_t) { return true; },
+ false)) {
connection_closed = true;
}
}
@@ -10680,6 +10732,7 @@ bool ClientImpl::create_and_connect_socket(Socket &socket,
}
bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
+ socket_tls_settings_version_ = tls_settings_version_;
return create_and_connect_socket(socket, error);
}
@@ -10729,6 +10782,28 @@ void ClientImpl::disconnect(bool gracefully) {
close_socket(socket_);
}
+bool ClientImpl::socket_reusable() {
+ if (!socket_.is_open()) { return false; }
+
+ auto is_alive = detail::is_socket_alive(socket_.sock);
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ if (is_alive && is_ssl() && tls::is_peer_closed(socket_.ssl, socket_.sock)) {
+ is_alive = false;
+ }
+#endif
+ if (!is_alive) {
+ // Peer seems gone — non-graceful shutdown to avoid SIGPIPE.
+ disconnect(/*gracefully=*/false);
+ return false;
+ }
+
+ if (is_ssl() && socket_tls_settings_version_ != tls_settings_version_) {
+ disconnect(/*gracefully=*/true);
+ return false;
+ }
+ return true;
+}
+
bool ClientImpl::read_response_line(Stream &strm, const Request &req,
Response &res,
bool skip_100_continue) const {
@@ -10806,25 +10881,7 @@ bool ClientImpl::send_(Request &req, Response &res, Error &error) {
// socket.
socket_should_be_closed_when_request_is_done_ = false;
- auto is_alive = false;
- if (socket_.is_open()) {
- is_alive = detail::is_socket_alive(socket_.sock);
-
-#ifdef CPPHTTPLIB_SSL_ENABLED
- if (is_alive && is_ssl()) {
- if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
- is_alive = false;
- }
- }
-#endif
-
- if (!is_alive) {
- // Peer seems gone — non-graceful shutdown to avoid SIGPIPE.
- disconnect(/*gracefully=*/false);
- }
- }
-
- if (!is_alive) {
+ if (!socket_reusable()) {
if (!ensure_socket_connection(socket_, error)) { return false; }
{
@@ -10987,20 +11044,7 @@ ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
{
std::lock_guard<std::mutex> guard(socket_mutex_);
- auto is_alive = false;
- if (socket_.is_open()) {
- is_alive = detail::is_socket_alive(socket_.sock);
-#ifdef CPPHTTPLIB_SSL_ENABLED
- if (is_alive && is_ssl()) {
- if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
- is_alive = false;
- }
- }
-#endif
- if (!is_alive) { disconnect(/*gracefully=*/false); }
- }
-
- if (!is_alive) {
+ if (!socket_reusable()) {
if (!ensure_socket_connection(socket_, handle.error)) {
handle.response.reset();
return handle;
@@ -11048,8 +11092,11 @@ ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
{
detail::BufferStream bstrm;
- if (detail::write_request_line(bstrm, req.method,
- proxy_target_prefix() + req.path) < 0) {
+ // The path follows the authority in absolute-form, so one that does not
+ // begin with '/' (e.g. "@evil.example/x") would name another host.
+ auto prefix = proxy_target_prefix();
+ if ((!prefix.empty() && req.path[0] != '/') ||
+ detail::write_request_line(bstrm, req.method, prefix + req.path) < 0) {
handle.error = Error::Write;
handle.response.reset();
return handle;
@@ -11070,7 +11117,7 @@ ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
}
if (!req.body.empty()) {
- if (strm.write(req.body.data(), req.body.size()) < 0) {
+ if (!detail::write_data(strm, req.body.data(), req.body.size())) {
handle.error = Error::Write;
handle.response.reset();
return handle;
@@ -11084,20 +11131,25 @@ ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
return handle;
}
- // Same framing check as ClientImpl::process_request(). A HEAD or bodyless
- // (204/304) response legitimately carries framing headers with no body.
- if (req.method != "HEAD" &&
- handle.response->status != StatusCode::NoContent_204 &&
- handle.response->status != StatusCode::NotModified_304 &&
- detail::has_conflicting_content_length(handle.response->headers)) {
+ handle.body_reader_.stream = handle.stream_;
+ handle.body_reader_.payload_max_length = payload_max_length_;
+
+ // RFC 9112 6.3: a response to HEAD, and a 204 or 304 response, ends with its
+ // header section, so none of its framing or coding headers apply.
+ if (req.method == "HEAD" ||
+ handle.response->status == StatusCode::NoContent_204 ||
+ handle.response->status == StatusCode::NotModified_304) {
+ handle.body_reader_.eof = true;
+ return handle;
+ }
+
+ // Same framing check as ClientImpl::process_request()
+ if (detail::has_conflicting_content_length(handle.response->headers)) {
handle.error = Error::Read;
handle.response.reset();
return handle;
}
- handle.body_reader_.stream = handle.stream_;
- handle.body_reader_.payload_max_length = payload_max_length_;
-
if (handle.response->has_header("Content-Length")) {
bool is_invalid = false;
auto content_length = detail::get_header_value_u64(
@@ -11116,7 +11168,7 @@ ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
auto content_encoding = detail::get_combined_header_value(
handle.response->headers, "Content-Encoding");
- if (!content_encoding.empty()) {
+ if (decompress_ && !content_encoding.empty()) {
// Same policy as prepare_content_receiver(): reject a coding we know about
// but were not built with, pass an unrecognized one through as-is.
handle.decompressor_ = detail::create_decompressor(content_encoding);
@@ -11262,6 +11314,15 @@ ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,
if (!is_space_or_tab(*p) && !fields::is_field_vchar(*p)) { return -1; }
}
+ // The bytes around the data are no payload, so they are counted on their
+ // own, as Go does: each chunk may spend 16 bytes and twice its data on
+ // them, which lets a stream send a byte at a time, and what goes past that
+ // in padding, leading zeros and extensions builds up to one header field.
+ overhead += lr.size() + 2;
+ overhead = overhead > 16 ? overhead - 16 : 0;
+ overhead = chunk_len > overhead / 2 ? 0 : overhead - 2 * chunk_len;
+ if (overhead > CPPHTTPLIB_HEADER_MAX_LENGTH) { return -1; }
+
if (chunk_len == 0) {
chunk_remaining = 0;
finished = true;
@@ -11302,10 +11363,18 @@ bool ChunkedDecoder::parse_trailers_into(Headers &dest,
return parse_trailers(lr, dest, src_headers);
}
-// A 3xx without a Location, such as a 304, is a final response
+// Only 301, 302, 303, 307 and 308 redirect. Any other 3xx, such as a 304, or
+// a redirect without a Location, is a final response.
bool is_redirect(const Response &res) {
- return 300 < res.status && res.status < 400 &&
- !res.get_header_value("Location").empty();
+ switch (res.status) {
+ case StatusCode::MovedPermanently_301:
+ case StatusCode::Found_302:
+ case StatusCode::SeeOther_303:
+ case StatusCode::TemporaryRedirect_307:
+ case StatusCode::PermanentRedirect_308:
+ return !res.get_header_value("Location").empty();
+ default: return false;
+ }
}
} // namespace detail
@@ -11340,6 +11409,13 @@ bool ClientImpl::handle_request(Stream &strm, Request &req,
auto prefix = proxy_target_prefix();
if (!prefix.empty()) {
+ // The path follows the authority in absolute-form, so one that does not
+ // begin with '/' (e.g. "@evil.example/x") would name another host.
+ if (req.path[0] != '/') {
+ error = Error::Write;
+ return false;
+ }
+
auto req2 = req;
req2.path = prefix + req.path;
ret = process_request(strm, req2, res, close_connection, error);
@@ -11360,9 +11436,10 @@ bool ClientImpl::handle_request(Stream &strm, Request &req,
// to call it from a different thread since it's a thread-safety issue
// to do these things to the socket if another thread is using the socket.
std::lock_guard<std::mutex> guard(socket_mutex_);
- // Also when the request asked for it (e.g. a body that was never sent),
- // so that a redirect or an auth retry below does not reuse the socket.
- if (socket_should_be_closed_when_request_is_done_ ||
+ // Also when the request asked for it (e.g. a body that was never sent,
+ // or "Connection: close"), so that a redirect or an auth retry below does
+ // not reuse the socket.
+ if (socket_should_be_closed_when_request_is_done_ || close_connection ||
detail::has_header_token(res.headers, "Connection", "close") ||
(res.version == "HTTP/1.0" && res.reason != "Connection established")) {
disconnect(/*gracefully=*/true);
@@ -11456,6 +11533,10 @@ bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
if (next_scheme.empty()) { next_scheme = scheme; }
if (next_host.empty()) { next_host = host_; }
if (next_path.empty()) { next_path = "/"; }
+ // An absolute or absolute-path location can have dot segments too.
+ if (next_path[0] == '/') {
+ next_path = detail::remove_dot_segments(next_path);
+ }
auto path = std::move(next_path) + next_query;
@@ -11520,7 +11601,7 @@ bool ClientImpl::create_redirect_client(
}
redirect_client.load_ca_cert_store(pem.data(), pem.size());
redirect_client.ca_cert_store_set_ = true;
- } else {
+ } else if (self.custom_ca_loaded_) {
redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(),
ca_cert_pem_.size());
}
@@ -11610,6 +11691,7 @@ void ClientImpl::setup_redirect_client(ClientType &client) {
// Copy network and socket settings
client.addr_map_ = addr_map_;
+ client.tunnel_addr_map_ = tunnel_addr_map_;
client.set_address_family(address_family_);
client.set_tcp_nodelay(tcp_nodelay_);
client.set_ipv6_v6only(ipv6_v6only_);
@@ -12107,8 +12189,7 @@ bool ClientImpl::process_request(Stream &strm, Request &req,
}
}
- auto redirect = follow_location_ && detail::is_redirect(res) &&
- res.status != StatusCode::NotModified_304;
+ auto redirect = follow_location_ && detail::is_redirect(res);
if (req.response_handler && !redirect) {
if (!req.response_handler(res)) {
@@ -13123,9 +13204,19 @@ void ClientImpl::set_path_encode(bool on) { path_encode_ = on; }
void
ClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
+ set_direct_hostname_addr_map(std::move(addr_map));
+}
+
+void ClientImpl::set_direct_hostname_addr_map(
+ std::map<std::string, std::string> addr_map) {
addr_map_ = std::move(addr_map);
}
+void ClientImpl::set_tunnel_hostname_addr_map(
+ std::map<std::string, std::string> addr_map) {
+ tunnel_addr_map_ = std::move(addr_map);
+}
+
void ClientImpl::set_default_headers(Headers headers) {
default_headers_ = std::move(headers);
}
@@ -13203,11 +13294,15 @@ void ClientImpl::set_proxy_digest_auth(const std::string &username,
}
void ClientImpl::enable_server_certificate_verification(bool enabled) {
+ if (server_certificate_verification_ == enabled) { return; }
server_certificate_verification_ = enabled;
+ tls_settings_changed();
}
void ClientImpl::enable_server_hostname_verification(bool enabled) {
+ if (server_hostname_verification_ == enabled) { return; }
server_hostname_verification_ = enabled;
+ tls_settings_changed();
}
void ClientImpl::enable_system_ca(bool enabled) {
@@ -13830,7 +13925,17 @@ socket_t Client::socket() const { return cli_->socket(); }
void
Client::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
- cli_->set_hostname_addr_map(std::move(addr_map));
+ cli_->set_direct_hostname_addr_map(std::move(addr_map));
+}
+
+void Client::set_direct_hostname_addr_map(
+ std::map<std::string, std::string> addr_map) {
+ cli_->set_direct_hostname_addr_map(std::move(addr_map));
+}
+
+void Client::set_tunnel_hostname_addr_map(
+ std::map<std::string, std::string> addr_map) {
+ cli_->set_tunnel_hostname_addr_map(std::move(addr_map));
}
void Client::set_default_headers(Headers headers) {
@@ -13964,8 +14069,10 @@ SSLServer::SSLServer(const PemMemory &pem) {
last_ssl_error_ = static_cast<int>(get_error());
free_context(ctx_);
ctx_ = nullptr;
- } else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) {
- if (!load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) {
+ } else if (pem.client_ca_pem) {
+ auto ca_len = pem.client_ca_pem_len ? pem.client_ca_pem_len
+ : strlen(pem.client_ca_pem);
+ if (!load_ca_pem(ctx_, pem.client_ca_pem, ca_len)) {
last_ssl_error_ = static_cast<int>(get_error());
free_context(ctx_);
ctx_ = nullptr;
@@ -14093,7 +14200,9 @@ bool SSLServer::update_certs_pem(const char *cert_pem,
return false;
}
if (client_ca_pem) {
- return tls::update_server_client_ca(ctx_, client_ca_pem);
+ if (!tls::update_server_client_ca(ctx_, client_ca_pem)) { return false; }
+ // As in the constructors, a client CA makes client certificates required
+ tls::set_verify_client(ctx_, true);
}
return true;
}
@@ -14194,6 +14303,10 @@ bool SSLClient::connect_with_proxy(
std::chrono::time_point<std::chrono::steady_clock> start_time,
Response &res, bool &success, Error &error) {
success = true;
+ auto connect_target =
+ detail::make_connect_request_target(tunnel_addr_map_, host_, port_);
+ auto retargeted = connect_target !=
+ detail::make_host_and_port_string_always_port(host_, port_);
Response proxy_res;
if (!detail::process_client_socket(
socket.sock, read_timeout_sec_, read_timeout_usec_,
@@ -14201,8 +14314,8 @@ bool SSLClient::connect_with_proxy(
start_time, [&](Stream &strm) {
Request req2;
req2.method = "CONNECT";
- req2.path =
- detail::make_host_and_port_string_always_port(host_, port_);
+ req2.path = connect_target;
+ if (retargeted) { req2.set_header("Host", connect_target); }
if (max_timeout_msec_ > 0) {
req2.start_time_ = std::chrono::steady_clock::now();
}
@@ -14241,8 +14354,8 @@ bool SSLClient::connect_with_proxy(
start_time, [&](Stream &strm) {
Request req3;
req3.method = "CONNECT";
- req3.path = detail::make_host_and_port_string_always_port(
- host_, port_);
+ req3.path = connect_target;
+ if (retargeted) { req3.set_header("Host", connect_target); }
req3.headers.insert(detail::make_digest_authentication_header(
req3, make_request_target(req3), auth, 1,
detail::random_string(10), proxy_digest_auth_username_,
@@ -14361,25 +14474,33 @@ void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
} else if (ca_cert_store) {
tls::free_ca_store(ca_cert_store);
}
+ custom_ca_loaded_ = true;
+ tls_settings_changed();
}
void
SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {
if (!ctx_) { return; }
server_certificate_verifier_ = verifier;
- // Under the lock that creating a session from ctx_ takes
- std::lock_guard<std::mutex> guard(ctx_mutex_);
- tls::set_verify_callback(ctx_, std::move(verifier));
+ {
+ // Under the lock that creating a session from ctx_ takes
+ std::lock_guard<std::mutex> guard(ctx_mutex_);
+ tls::set_verify_callback(ctx_, std::move(verifier));
+ }
+ tls_settings_changed();
}
void SSLClient::set_session_verifier(
std::function<SSLVerifierResponse(tls::session_t)> verifier) {
session_verifier_ = std::move(verifier);
+ tls_settings_changed();
}
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
void SSLClient::enable_windows_certificate_verification(bool enabled) {
+ if (enable_windows_cert_verification_ == enabled) { return; }
enable_windows_cert_verification_ = enabled;
+ tls_settings_changed();
}
#endif
@@ -14393,6 +14514,8 @@ void SSLClient::load_ca_cert_store(const char *ca_cert,
ca_cert_pem_.append(ca_cert, size);
tls::load_ca_pem(ctx_, ca_cert, size);
}
+ custom_ca_loaded_ = true;
+ tls_settings_changed();
}
bool SSLClient::load_certs() {
@@ -14403,10 +14526,9 @@ bool SSLClient::load_certs() {
std::call_once(initialize_cert_, [&]() {
std::lock_guard<std::mutex> guard(ctx_mutex_);
- ret = detail::load_client_ca_config(
- ctx_, ca_cert_file_path_, ca_cert_dir_path_,
- !ca_cert_pem_.empty() || ca_cert_store_set_, system_ca_mode_,
- last_backend_error_);
+ ret = detail::load_client_ca_config(ctx_, ca_cert_file_path_,
+ ca_cert_dir_path_, custom_ca_loaded_,
+ system_ca_mode_, last_backend_error_);
});
return ret;
@@ -14432,7 +14554,7 @@ bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
options.windows_cert_verification =
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_;
+ ca_cert_dir_path_.empty() && !custom_ca_loaded_;
// 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 =
@@ -14647,15 +14769,75 @@ bool enumerate_windows_system_certs(Callback cb) {
#endif
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
-// Enumerate macOS Keychain certificates and call callback with DER data
+// The trust a certificate's Trust Settings give it for SSL server
+// verification (SecTrustSettings.h). An empty array trusts it as a root.
+// Otherwise the first usage-constraints dictionary that applies decides: one
+// naming a policy other than SSL, a host name or an application does not
+// apply, and a result left out means TrustRoot. Keychain Access writes "Never
+// Trust" as kSecTrustSettingsResultDeny.
+SecTrustSettingsResult
+macos_ssl_trust_settings_result(CFArrayRef trust_settings) {
+ auto count = CFArrayGetCount(trust_settings);
+ if (count == 0) { return kSecTrustSettingsResultTrustRoot; }
+
+ for (CFIndex i = 0; i < count; i++) {
+ auto value = CFArrayGetValueAtIndex(trust_settings, i);
+ if (!value || CFGetTypeID(value) != CFDictionaryGetTypeID()) { continue; }
+ auto constraint = static_cast<CFDictionaryRef>(value);
+
+ auto policy_value =
+ CFDictionaryGetValue(constraint, kSecTrustSettingsPolicy);
+ if (policy_value) {
+ auto policy = static_cast<SecPolicyRef>(const_cast<void *>(policy_value));
+ auto is_ssl = false;
+ if (auto properties = SecPolicyCopyProperties(policy)) {
+ auto oid = CFDictionaryGetValue(properties, kSecPolicyOid);
+ is_ssl = oid && CFEqual(oid, kSecPolicyAppleSSL);
+ CFRelease(properties);
+ }
+ if (!is_ssl) { continue; }
+ }
+
+ if (CFDictionaryContainsKey(constraint, kSecTrustSettingsPolicyString) ||
+ CFDictionaryContainsKey(constraint, kSecTrustSettingsApplication)) {
+ continue;
+ }
+
+ auto result = kSecTrustSettingsResultTrustRoot;
+ auto number = static_cast<CFNumberRef>(
+ CFDictionaryGetValue(constraint, kSecTrustSettingsResult));
+ if (number) {
+ SInt32 number_value = 0;
+ if (!CFNumberGetValue(number, kCFNumberSInt32Type, &number_value)) {
+ continue;
+ }
+ result = static_cast<SecTrustSettingsResult>(number_value);
+ }
+ if (result == kSecTrustSettingsResultTrustRoot ||
+ result == kSecTrustSettingsResultTrustAsRoot ||
+ result == kSecTrustSettingsResultDeny) {
+ return result;
+ }
+ }
+
+ return kSecTrustSettingsResultUnspecified;
+}
+
+// Enumerate the macOS Keychain certificates trusted for SSL and call callback
+// with DER data. A certificate with Trust Settings is not trusted just by
+// having them: the settings say whether it is, and a per-user setting
+// overrides the admin domain's, which overrides the system domain's (whose
+// roots are all trusted). So a system root the user set to "Never Trust" is
+// left out, as is one trusted only for S/MIME or code signing.
template <typename Callback>
bool enumerate_macos_keychain_certs(Callback cb) {
- bool loaded = false;
const SecTrustSettingsDomain domains[] = {
- kSecTrustSettingsDomainSystem,
- kSecTrustSettingsDomainAdmin,
kSecTrustSettingsDomainUser,
+ kSecTrustSettingsDomainAdmin,
+ kSecTrustSettingsDomainSystem,
};
+ std::set<std::string> decided;
+ std::vector<std::string> trusted;
for (auto domain : domains) {
CFArrayRef certs = nullptr;
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
@@ -14668,16 +14850,43 @@ bool enumerate_macos_keychain_certs(Callback cb) {
SecCertificateRef cert =
(SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
CFDataRef data = SecCertificateCopyData(cert);
- if (data) {
- if (cb(CFDataGetBytePtr(data),
- static_cast<size_t>(CFDataGetLength(data)))) {
- loaded = true;
+ if (!data) { continue; }
+ std::string der(reinterpret_cast<const char *>(CFDataGetBytePtr(data)),
+ static_cast<size_t>(CFDataGetLength(data)));
+ CFRelease(data);
+
+ // Decided by a domain that overrides this one
+ if (decided.find(der) != decided.end()) { continue; }
+
+ auto result = kSecTrustSettingsResultTrustRoot;
+ if (domain != kSecTrustSettingsDomainSystem) {
+ result = kSecTrustSettingsResultUnspecified;
+ CFArrayRef trust_settings = nullptr;
+ if (SecTrustSettingsCopyTrustSettings(cert, domain, &trust_settings) ==
+ errSecSuccess &&
+ trust_settings) {
+ result = macos_ssl_trust_settings_result(trust_settings);
}
- CFRelease(data);
+ if (trust_settings) CFRelease(trust_settings);
+ }
+ // Neither trusted nor distrusted here; a domain below may decide
+ if (result == kSecTrustSettingsResultUnspecified) { continue; }
+
+ decided.insert(der);
+ if (result == kSecTrustSettingsResultTrustRoot ||
+ result == kSecTrustSettingsResultTrustAsRoot) {
+ trusted.push_back(std::move(der));
}
}
CFRelease(certs);
}
+
+ bool loaded = false;
+ for (const auto &der : trusted) {
+ if (cb(reinterpret_cast<const unsigned char *>(der.data()), der.size())) {
+ loaded = true;
+ }
+ }
return loaded;
}
#endif
@@ -14866,6 +15075,30 @@ ErrorCode map_ssl_error(int ssl_error, int &out_errno) {
}
}
+// Fills in a failed SSL_connect(). One that the server certificate's
+// verification stopped reports the verify result, as the other backends and
+// a check after the handshake do.
+void fill_connect_error(SSL *ssl, int ret, TlsError &err) {
+ auto ssl_err = SSL_get_error(ssl, ret);
+ auto error = ERR_get_error();
+ err.code = map_ssl_error(ssl_err, err.sys_errno);
+ err.backend_code = error;
+ if (ssl_err != SSL_ERROR_SSL || ERR_GET_LIB(error) != ERR_LIB_SSL ||
+ ERR_GET_REASON(error) != SSL_R_CERTIFICATE_VERIFY_FAILED) {
+ return;
+ }
+ // A certificate only a verify callback turned down carries no verdict of
+ // its own, and fails as any other handshake does
+ auto verify_result = SSL_get_verify_result(ssl);
+ if (verify_result == X509_V_OK || verify_result == X509_V_ERR_UNSPECIFIED) {
+ return;
+ }
+ err.code = verify_result == X509_V_ERR_HOSTNAME_MISMATCH
+ ? ErrorCode::HostnameMismatch
+ : ErrorCode::CertVerifyFailed;
+ err.backend_code = static_cast<uint64_t>(verify_result);
+}
+
// Helper: Create client CA list from PEM string
// Returns a new STACK_OF(X509_NAME)* or nullptr on failure
// Caller takes ownership of returned list
@@ -14941,7 +15174,14 @@ int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
verify_ctx.error_string =
(error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr;
- return run_verify_callback(*callback, verify_ctx) ? 1 : 0;
+ auto accepted = run_verify_callback(*callback, verify_ctx);
+ // A client verifying the server during the handshake, which is what
+ // set_verify_client(ctx, true) asks for there, keeps the built-in verdict
+ if (!SSL_is_server(ssl) &&
+ (SSL_get_verify_mode(ssl) & SSL_VERIFY_FAIL_IF_NO_PEER_CERT)) {
+ accepted = accepted && preverify_ok;
+ }
+ return accepted ? 1 : 0;
}
// Sets the verify mode from whether the peer's certificate is required and
@@ -15080,36 +15320,14 @@ bool load_system_certs(ctx_t ctx) {
auto store = SSL_CTX_get_cert_store(ssl_ctx);
if (!store) return false;
- bool loaded_any = false;
- const SecTrustSettingsDomain domains[] = {
- kSecTrustSettingsDomainSystem,
- kSecTrustSettingsDomainAdmin,
- kSecTrustSettingsDomainUser,
- };
- for (auto domain : domains) {
- CFArrayRef certs = nullptr;
- if (SecTrustSettingsCopyCertificates(domain, &certs) != errSecSuccess ||
- !certs) {
- if (certs) CFRelease(certs);
- continue;
- }
- auto count = CFArrayGetCount(certs);
- for (CFIndex i = 0; i < count; i++) {
- auto cert = reinterpret_cast<SecCertificateRef>(
- const_cast<void *>(CFArrayGetValueAtIndex(certs, i)));
- CFDataRef der = SecCertificateCopyData(cert);
- if (der) {
- const unsigned char *data = CFDataGetBytePtr(der);
- auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der));
- if (x509) {
- if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
- X509_free(x509);
- }
- CFRelease(der);
- }
- }
- CFRelease(certs);
- }
+ auto loaded_any = impl::enumerate_macos_keychain_certs(
+ [&](const unsigned char *data, size_t len) {
+ auto x509 = d2i_X509(nullptr, &data, static_cast<long>(len));
+ if (!x509) { return false; }
+ auto added = X509_STORE_add_cert(store, x509) == 1;
+ X509_free(x509);
+ return added;
+ });
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
#else
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
@@ -15217,14 +15435,20 @@ void free_session(session_t session) {
}
bool set_sni(session_t session, const char *hostname,
- bool /*verify_hostname*/) {
+ bool verify_hostname) {
if (!session || !hostname) return false;
auto ssl = static_cast<SSL *>(session);
- // Set SNI (Server Name Indication) only - does not enable verification.
- // OpenSSL never binds identity checking to SNI (that happens post-
- // handshake in setup_client_tls_session()), so verify_hostname is unused.
+ // The handshake's chain verification then checks the name as well
+ if (verify_hostname) {
+ auto param = SSL_get0_param(ssl);
+ X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NEVER_CHECK_SUBJECT);
+ if (X509_VERIFY_PARAM_set1_host(param, hostname, strlen(hostname)) != 1) {
+ return false;
+ }
+ }
+
#if defined(OPENSSL_IS_BORINGSSL)
return SSL_set_tlsext_host_name(ssl, hostname) == 1;
#else
@@ -15244,9 +15468,7 @@ TlsError connect(session_t session) {
if (ret == 1) {
err.code = ErrorCode::Success;
} else {
- auto ssl_err = SSL_get_error(ssl, ret);
- err.code = impl::map_ssl_error(ssl_err, err.sys_errno);
- err.backend_code = ERR_get_error();
+ impl::fill_connect_error(ssl, ret, err);
}
return err;
}
@@ -15302,10 +15524,7 @@ bool connect_nonblocking(session_t session, socket_t sock,
break;
default: break;
}
- if (err) {
- err->code = impl::map_ssl_error(ssl_err, err->sys_errno);
- err->backend_code = ERR_get_error();
- }
+ if (err) { impl::fill_connect_error(ssl, res, *err); }
return false;
}
if (err) { err->code = ErrorCode::Success; }
@@ -15476,7 +15695,8 @@ bool verify_hostname(cert_t cert, const char *hostname) {
if (detail::is_ip_address(hostname)) {
return X509_check_ip_asc(x509, hostname, 0) == 1;
}
- return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1;
+ return X509_check_host(x509, hostname, strlen(hostname),
+ X509_CHECK_FLAG_NEVER_CHECK_SUBJECT, nullptr) == 1;
}
uint64_t hostname_mismatch_code() {
@@ -15967,6 +16187,11 @@ ErrorCode map_mbedtls_error(int ret, int &out_errno,
out_errno = errno;
return ErrorCode::SyscallError;
}
+ if (ret == MBEDTLS_ERR_SSL_CONN_EOF) {
+ // EOF without close_notify
+ out_errno = 0;
+ return ErrorCode::SyscallError;
+ }
if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {
// Unlike OpenSSL/wolfSSL, Mbed TLS folds the CN/SAN identity check into
// the handshake's chain verification (see set_sni()); a mismatch there
@@ -16054,7 +16279,7 @@ int mbedtls_net_recv_cb(void *ctx, unsigned char *buf, size_t len) {
return MBEDTLS_ERR_NET_RECV_FAILED;
}
#endif
- if (ret == 0) { return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; }
+ // EOF is MBEDTLS_ERR_SSL_CONN_EOF; only a close_notify is a clean end
return static_cast<int>(ret);
}
@@ -16605,6 +16830,9 @@ TlsError connect(session_t session) {
}
TlsError accept(session_t session) {
+ // A failed handshake leaves its SNI behind, so start without it
+ impl::mbedpending_sni().clear();
+
// Same as connect for Mbed TLS - handshake works for both client and server
auto result = connect(session);
@@ -16612,7 +16840,6 @@ TlsError accept(session_t session) {
if (result.code == ErrorCode::Success && session) {
auto msession = static_cast<impl::MbedTlsSession *>(session);
msession->sni_hostname = std::move(impl::mbedpending_sni());
- impl::mbedpending_sni().clear();
}
return result;
@@ -16657,6 +16884,9 @@ bool connect_nonblocking(session_t session, socket_t sock,
bool accept_nonblocking(session_t session, socket_t sock,
time_t timeout_sec, time_t timeout_usec,
TlsError *err) {
+ // A failed handshake leaves its SNI behind, so start without it
+ impl::mbedpending_sni().clear();
+
// Same implementation as connect for Mbed TLS
bool result =
connect_nonblocking(session, sock, timeout_sec, timeout_usec, err);
@@ -16665,7 +16895,6 @@ bool accept_nonblocking(session_t session, socket_t sock,
if (result && session) {
auto msession = static_cast<impl::MbedTlsSession *>(session);
msession->sni_hostname = std::move(impl::mbedpending_sni());
- impl::mbedpending_sni().clear();
}
return result;
@@ -16706,10 +16935,8 @@ ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
return static_cast<ssize_t>(ret);
}
- if (ret == 0) {
- err.code = ErrorCode::PeerClosed;
- return 0;
- }
+ // 0 means EOF without close_notify, which is an error as on OpenSSL
+ if (ret == 0) { ret = MBEDTLS_ERR_SSL_CONN_EOF; }
err.code = impl::map_mbedtls_error(ret, err.sys_errno, 0);
err.backend_code = static_cast<uint64_t>(-ret);
@@ -16858,7 +17085,6 @@ bool verify_hostname(cert_t cert, const char *hostname) {
// Check Subject Alternative Names (SAN). Mbed TLS keeps the GeneralName type
// in buf.tag and the raw value in buf.p / buf.len.
- auto has_dns_san = false;
for (auto san = &mcert->subject_alt_names; san; san = san->next) {
if (!san->buf.p || san->buf.len == 0) { continue; }
const unsigned char *p = san->buf.p;
@@ -16873,24 +17099,11 @@ bool verify_hostname(cert_t cert, const char *hostname) {
return true;
}
} else if (san_type == MBEDTLS_X509_SAN_DNS_NAME) {
- has_dns_san = true;
std::string san_name(reinterpret_cast<const char *>(p), len);
if (detail::match_hostname(san_name, host_str)) { return true; }
}
}
- // Fallback: the Common Names (CN) in the subject, as X509_check_host does it:
- // never for an IP host, which only an iPAddress SAN names, and never next to
- // a DNS SAN (RFC 6125 6.4.4).
- if (is_ip || has_dns_san) { return false; }
- for (auto name = &mcert->subject; name; name = name->next) {
- if (MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &name->oid) == 0) {
- std::string cn(reinterpret_cast<const char *>(name->val.p),
- name->val.len);
- if (detail::match_hostname(cn, host_str)) { return true; }
- }
- }
-
return false;
}
@@ -17071,24 +17284,28 @@ std::string error_string(uint64_t code) {
}
ca_store_t create_ca_store(const char *pem, size_t len) {
+#ifdef CPPHTTPLIB_MBEDTLS_V4
+ // Parsing certificates needs PSA Crypto, and no context may exist yet
+ if (!detail::ensure_mbedtls_psa_crypto()) { return nullptr; }
+#endif
+
+ if (!pem) { return nullptr; }
+
+ // mbedtls_x509_crt_parse expects null-terminated PEM
+ std::string pem_str(pem, len);
+
auto *ca_chain = new (std::nothrow) mbedtls_x509_crt;
if (!ca_chain) { return nullptr; }
mbedtls_x509_crt_init(ca_chain);
- // mbedtls_x509_crt_parse expects null-terminated PEM
- int ret = mbedtls_x509_crt_parse(ca_chain,
- reinterpret_cast<const unsigned char *>(pem),
- len + 1); // +1 for null terminator
+ auto ret = mbedtls_x509_crt_parse(
+ ca_chain, reinterpret_cast<const unsigned char *>(pem_str.c_str()),
+ pem_str.size() + 1);
if (ret != 0) {
- // Try without +1 in case PEM is already null-terminated
- ret = mbedtls_x509_crt_parse(
- ca_chain, reinterpret_cast<const unsigned char *>(pem), len);
- if (ret != 0) {
- mbedtls_x509_crt_free(ca_chain);
- delete ca_chain;
- return nullptr;
- }
+ mbedtls_x509_crt_free(ca_chain);
+ delete ca_chain;
+ return nullptr;
}
return static_cast<ca_store_t>(ca_chain);
@@ -17326,6 +17543,8 @@ ErrorCode map_wolfssl_error(WOLFSSL *ssl, int ssl_error,
case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite;
case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed;
case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError;
+ // EOF without close_notify
+ case SOCKET_PEER_CLOSED_E: out_errno = 0; return ErrorCode::SyscallError;
default:
if (ssl) {
// wolfSSL stores the low-level error code as a negative value.
@@ -17438,7 +17657,13 @@ int wolfssl_verify_callback(int preverify_ok,
verify_ctx.error_string = nullptr;
}
- bool accepted = run_verify_callback(*callback, verify_ctx);
+ auto accepted = run_verify_callback(*callback, verify_ctx);
+ // A client verifying the server during the handshake, which is what
+ // set_verify_client(ctx, true) asks for there, keeps the built-in verdict
+ if (!wolfSSL_is_server(ssl) &&
+ (wolfSSL_get_verify_mode(ssl) & WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT)) {
+ accepted = accepted && preverify_ok;
+ }
return accepted ? 1 : 0;
}
@@ -17890,13 +18115,10 @@ ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
return static_cast<ssize_t>(ret);
}
- if (ret == 0) {
- err.code = ErrorCode::PeerClosed;
- return 0;
- }
-
+ // 0 is also returned for EOF without close_notify; get_error tells them apart
int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno);
+ if (err.code == ErrorCode::PeerClosed) { return 0; }
err.backend_code = static_cast<uint64_t>(ssl_error);
impl::wolfssl_last_error() = err.backend_code;
return -1;
@@ -18028,7 +18250,6 @@ bool verify_hostname(cert_t cert, const char *hostname) {
auto is_ip = ip_len > 0;
// Check Subject Alternative Names
- auto has_dns_san = false;
auto *san_names = static_cast<WOLF_STACK_OF(WOLFSSL_GENERAL_NAME) *>(
wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr));
@@ -18041,7 +18262,6 @@ bool verify_hostname(cert_t cert, const char *hostname) {
if (!is_ip && names->type == WOLFSSL_GEN_DNS) {
// DNS name
- has_dns_san = true;
unsigned char *dns_name = nullptr;
int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, names->d.dNSName);
if (dns_name && dns_len > 0) {
@@ -18068,12 +18288,7 @@ bool verify_hostname(cert_t cert, const char *hostname) {
wolfSSL_sk_free(san_names);
}
- // Fallback: the Common Name (CN) in the subject, as X509_check_host does it:
- // never for an IP host, which only an iPAddress SAN names, and never next to
- // a DNS SAN (RFC 6125 6.4.4).
- if (is_ip || has_dns_san) { return false; }
- auto cn = get_cert_subject_cn(cert);
- return !cn.empty() && detail::match_hostname(cn, host_str);
+ return false;
}
uint64_t hostname_mismatch_code() {
@@ -18860,6 +19075,9 @@ void WebSocketClient::prepare_default_headers(Request &req) {
}
detail::add_default_user_agent_header(req);
+ detail::add_authorization_header(req, basic_auth_username_,
+ basic_auth_password_,
+ bearer_token_auth_token_, false);
}
Result WebSocketClient::connect() {
@@ -18985,9 +19203,19 @@ void WebSocketClient::set_interface(const std::string &intf) {
void WebSocketClient::set_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
+ set_direct_hostname_addr_map(std::move(addr_map));
+}
+
+void WebSocketClient::set_direct_hostname_addr_map(
+ std::map<std::string, std::string> addr_map) {
addr_map_ = std::move(addr_map);
}
+void WebSocketClient::set_tunnel_hostname_addr_map(
+ std::map<std::string, std::string> addr_map) {
+ tunnel_addr_map_ = std::move(addr_map);
+}
+
bool WebSocketClient::is_proxy_enabled() const {
if (proxy_host_.empty() || proxy_port_ == -1) { return false; }
if (no_proxy_entries_.empty()) { return true; }
@@ -19037,7 +19265,8 @@ bool WebSocketClient::connect_with_proxy(Response &proxy_res,
Error &error) {
Request req;
req.method = "CONNECT";
- req.path = detail::make_host_and_port_string_always_port(host_, port_);
+ req.path =
+ detail::make_connect_request_target(tunnel_addr_map_, host_, port_);
req.headers.emplace("Host", req.path);
detail::add_default_user_agent_header(req);
detail::add_authorization_header(req, proxy_basic_auth_username_,
@@ -19071,6 +19300,16 @@ bool WebSocketClient::connect_with_proxy(Response &proxy_res,
return true;
}
+void WebSocketClient::set_basic_auth(const std::string &username,
+ const std::string &password) {
+ basic_auth_username_ = username;
+ basic_auth_password_ = password;
+}
+
+void WebSocketClient::set_bearer_token_auth(const std::string &token) {
+ bearer_token_auth_token_ = token;
+}
+
void WebSocketClient::set_proxy(const std::string &host, int port) {
proxy_host_ = host;
proxy_port_ = port;
@@ -19115,18 +19354,18 @@ void WebSocketClient::set_ca_cert_store(tls::ca_store_t store) {
if (store && tls_ctx_) {
// set_ca_store takes ownership of store
tls::set_ca_store(tls_ctx_, store);
- custom_ca_loaded_ = true;
} else if (store) {
tls::free_ca_store(store);
}
+ custom_ca_loaded_ = true;
}
void WebSocketClient::load_ca_cert_store(const char *ca_cert,
std::size_t size) {
if (tls_ctx_ && ca_cert && size > 0) {
tls::load_ca_pem(tls_ctx_, ca_cert, size);
- custom_ca_loaded_ = true;
}
+ custom_ca_loaded_ = true;
}
void
diff --git a/vendor/cpp-httplib/httplib.h b/vendor/cpp-httplib/httplib.h
index 4ab2b5ad4..de77674ab 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.62.0"
-#define CPPHTTPLIB_VERSION_NUM "0x003e00"
+#define CPPHTTPLIB_VERSION "0.64.0"
+#define CPPHTTPLIB_VERSION_NUM "0x004000"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -142,6 +142,12 @@
#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (100 * 1024 * 1024) // 100MB
#endif
+// How much of an unread request body the server skips to reuse the connection,
+// 0 for no limit
+#ifndef CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH
+#define CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH (256 * 1024) // 256KB
+#endif
+
#ifndef CPPHTTPLIB_SSE_MAX_EVENT_LENGTH
#define CPPHTTPLIB_SSE_MAX_EVENT_LENGTH (16 * 1024 * 1024) // 16MB
#endif
@@ -989,6 +995,30 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
return true;
}
+// Removes "." and ".." segments from a path that starts with '/' (RFC 3986
+// section 5.2.4).
+inline std::string remove_dot_segments(const std::string &path) {
+ std::string out;
+ size_t i = 0;
+ while (i < path.size()) {
+ auto next = (std::min)(path.find('/', i + 1), path.size());
+ auto segment = path.substr(i + 1, next - i - 1);
+ auto is_last = next == path.size();
+ if (segment == "." || segment == "..") {
+ if (segment == "..") {
+ out.erase((std::min)(out.rfind('/'), out.size()));
+ }
+ if (is_last) { out += '/'; }
+ } else {
+ out += '/';
+ out += segment;
+ }
+ i = next;
+ }
+ if (out.empty()) { out = "/"; }
+ return out;
+}
+
// 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.
@@ -1009,26 +1039,8 @@ inline std::string resolve_relative_location(const std::string &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);
+ return remove_dot_segments(merged.substr(0, path_end)) +
+ merged.substr(path_end);
}
} // namespace detail
@@ -2357,6 +2369,8 @@ public:
Server &set_payload_max_length(size_t length);
+ Server &set_compress(bool on);
+
Server &set_static_file_compression(bool on);
Server &set_static_file_compression_min_length(size_t length);
Server &set_static_file_compression_max_length(size_t length);
@@ -2392,10 +2406,11 @@ protected:
// Runs the per-connection serving loop and stops an exception thrown by a
// user callback from escaping the worker thread.
//
- // process_request() wraps only routing() in a try/catch. Content providers,
- // the post-routing, error, logging and expect-100 handlers and WebSocket
- // handlers all run outside it, and the task queue calls the job without a
- // catch, so an exception from any of those would terminate the process.
+ // process_request() wraps only routing() and the hooks before a WebSocket
+ // upgrade in a try/catch. Content providers, the post-routing, error,
+ // logging and expect-100 handlers and WebSocket handlers all run outside
+ // it, and the task queue calls the job without a catch, so an exception
+ // from any of those would terminate the process.
//
// No 500 is possible here: by the time a content provider runs, the status
// line and headers are already on the wire. Report it through the error
@@ -2431,6 +2446,7 @@ protected:
time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
+ bool compress_ = true;
bool static_file_compression_ = false;
size_t static_file_compression_min_length_ =
CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH;
@@ -2481,6 +2497,9 @@ private:
bool routing(Request &req, Response &res, Stream &strm,
bool skip_pre_routing);
+#ifndef CPPHTTPLIB_NO_EXCEPTIONS
+ bool handle_exception(Request &req, Response &res);
+#endif
bool handle_file_request(Request &req, Response &res);
bool check_if_not_modified(const Request &req, Response &res,
const std::string &etag, time_t mtime) const;
@@ -2908,7 +2927,12 @@ public:
size_t is_socket_open() const;
socket_t socket() const;
+ [[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
+ void
+ set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
+ void
+ set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_default_headers(Headers headers);
@@ -2997,6 +3021,15 @@ protected:
void close_socket(Socket &socket);
void disconnect(bool gracefully);
+ // Under socket_mutex_: whether the connection kept alive can take another
+ // request. One the peer has closed, or one set up under TLS settings changed
+ // since, is closed instead.
+ bool socket_reusable();
+
+ // TLS settings apply as a connection is set up, so a change makes the
+ // connection kept alive unfit for the next request.
+ void tls_settings_changed() { tls_settings_version_++; }
+
std::string make_request_target(const Request &req) const;
bool process_request(Stream &strm, Request &req, Response &res,
bool close_connection, Error &error);
@@ -3023,11 +3056,17 @@ protected:
size_t socket_requests_in_flight_ = 0;
std::thread::id socket_requests_are_from_thread_ = std::thread::id();
bool socket_should_be_closed_when_request_is_done_ = false;
+ size_t socket_tls_settings_version_ = 0;
+
+ std::atomic<size_t> tls_settings_version_{0};
// Hostname to connection target map. The value is an IP literal or another
// hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_;
+ // Hostname to CONNECT target map, used only through a proxy.
+ std::map<std::string, std::string> tunnel_addr_map_;
+
// Default headers
Headers default_headers_;
@@ -3317,7 +3356,12 @@ public:
size_t is_socket_open() const;
socket_t socket() const;
+ [[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
+ void
+ set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
+ void
+ set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_default_headers(Headers headers);
@@ -3527,9 +3571,13 @@ private:
// Tracks whether a custom CA store was applied via set_ca_cert_store(),
// since the store handle itself is owned by ctx_ and leaves no other trace.
- // Used to keep custom CA configuration exclusive with system CA loading.
bool ca_cert_store_set_ = false;
+ // Set by set_ca_cert_store() and load_ca_cert_store() even when they bring
+ // no certificate, so that a CA that failed to come through fails
+ // verification instead of letting the system CAs in.
+ bool custom_ca_loaded_ = false;
+
std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
// Like ca_cert_store_set_, keeps what ctx_ cannot report back: the verifier
@@ -3768,6 +3816,8 @@ template <typename T>
bool check_and_write_headers(Stream &strm, Headers &headers, T header_writer,
Error &error);
+bool write_data(Stream &strm, const char *d, size_t l);
+
std::string trim_copy(const std::string &s);
void divide(
@@ -4015,6 +4065,7 @@ struct ChunkedDecoder {
char line_buf[64];
size_t last_chunk_total = 0;
size_t last_chunk_offset = 0;
+ size_t overhead = 0;
explicit ChunkedDecoder(Stream &s);
@@ -4466,7 +4517,7 @@ public:
// State accessors
bool is_connected() const;
- const std::string &last_event_id() const;
+ std::string last_event_id() const;
// Blocking start - runs event loop with auto-reconnect
void start();
@@ -4504,7 +4555,9 @@ private:
// State
std::atomic<bool> running_{false};
std::atomic<bool> connected_{false};
+ // Written by the event loop, which start_async() runs on its own thread
std::string last_event_id_;
+ mutable std::mutex last_event_id_mutex_;
// Async support
std::thread async_thread_;
@@ -4708,7 +4761,15 @@ public:
set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
void set_interface(const std::string &intf);
+ [[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
+ void
+ set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
+ void
+ set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
+
+ void set_basic_auth(const std::string &username, const std::string &password);
+ void set_bearer_token_auth(const std::string &token);
void set_proxy(const std::string &host, int port);
void set_proxy_basic_auth(const std::string &username,
@@ -4780,6 +4841,13 @@ private:
// hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_;
+ // Hostname to CONNECT target map, used only through a proxy.
+ std::map<std::string, std::string> tunnel_addr_map_;
+
+ std::string basic_auth_username_;
+ std::string basic_auth_password_;
+ std::string bearer_token_auth_token_;
+
std::string proxy_host_;
int proxy_port_ = -1;
std::string proxy_basic_auth_username_;
@@ -4793,6 +4861,9 @@ private:
tls::session_t tls_session_ = nullptr;
std::string ca_cert_file_path_;
std::string ca_cert_dir_path_;
+ // Set by set_ca_cert_store() and load_ca_cert_store() even when they bring
+ // no certificate, so that a CA that failed to come through fails
+ // verification instead of letting the system CAs in.
bool custom_ca_loaded_ = false;
bool certs_loaded_ = false;
SystemCAMode system_ca_mode_ = SystemCAMode::Auto;