From 1ab23c33b7599e9e73fdcd6584985ae02ce8da83 Mon Sep 17 00:00:00 2001 From: "Alessandro de Oliveira Faria (A.K.A.CABELO)" Date: Fri, 30 Oct 2026 07:49:58 -0300 Subject: [PATCH] vendor : update cpp-httplib to 0.64.0 (#31400) --- scripts/sync_vendor.py | 2 +- vendor/cpp-httplib/httplib.cpp | 1153 +++++++++++++++++++------------- vendor/cpp-httplib/httplib.h | 127 +++- 3 files changed, 796 insertions(+), 486 deletions(-) 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 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 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 &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(len); if (mask) { header[1] |= 0x80; } - if (strm.write(reinterpret_cast(header), 2) < 0) { return false; } + if (!write_data(strm, reinterpret_cast(header), 2)) { + return false; + } } else if (len <= 0xFFFF) { header[1] = 126; if (mask) { header[1] |= 0x80; } - if (strm.write(reinterpret_cast(header), 2) < 0) { return false; } + if (!write_data(strm, reinterpret_cast(header), 2)) { + return false; + } uint8_t ext[2]; ext[0] = static_cast((len >> 8) & 0xFF); ext[1] = static_cast(len & 0xFF); - if (strm.write(reinterpret_cast(ext), 2) < 0) { return false; } + if (!write_data(strm, reinterpret_cast(ext), 2)) { return false; } } else { header[1] = 127; if (mask) { header[1] |= 0x80; } - if (strm.write(reinterpret_cast(header), 2) < 0) { return false; } + if (!write_data(strm, reinterpret_cast(header), 2)) { + return false; + } uint8_t ext[8]; for (int i = 7; i >= 0; i--) { ext[7 - i] = static_cast((static_cast(len) >> (i * 8)) & 0xFF); } - if (strm.write(reinterpret_cast(ext), 8) < 0) { return false; } + if (!write_data(strm, reinterpret_cast(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(mask_key), 4) < 0) { return false; } + if (!write_data(strm, reinterpret_cast(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(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; + if (path.empty() || path[0] != '/') { return false; } - // Skip slash - while (i < path.size() && path[i] == '/') { - i++; - } - - 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 &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 &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(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 guard; if (options.ctx_mutex) { guard = std::unique_lock(*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(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(verify_result)); + return fail_tls(ErrorCode::CertVerifyFailed, + static_cast(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 &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 &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::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"; } + 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; + // 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); + + 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; + } + + if (stat.is_file()) { + for (const auto &kv : entry.headers) { + res.set_header(kv.first, kv.second); } - detail::FileStat stat(path); - - 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 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()); - } + auto mtime = stat.mtime(); - // 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 last_modified = detail::file_mtime_to_http_date(mtime); + if (!last_modified.empty()) { + res.set_header("Last-Modified", last_modified); + } - auto mtime = stat.mtime(); + if (check_if_not_modified(req, res, etag, mtime)) { return true; } - 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; } - - check_if_range(req, etag, mtime); - - auto mm = std::make_shared(path.c_str()); - if (!mm->is_open()) { - output_error_log(Error::OpenFile, &req); - return false; - } - - detail::set_file_content_provider(res, mm, content_type, encoding); - - if (req.method != "HEAD" && file_request_handler_) { - file_request_handler_(req, res); - } - - return true; - } else { + auto mm = std::make_shared(path.c_str()); + if (!mm->is_open()) { output_error_log(Error::OpenFile, &req); + return false; } + + detail::set_file_content_provider(res, mm, content_type, encoding); + + 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(new detail::WebSocketSSLStream( - strm.socket(), const_cast(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(new detail::WebSocketSSLStream( + strm.socket(), const_cast(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 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 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 addr_map) { + set_direct_hostname_addr_map(std::move(addr_map)); +} + +void ClientImpl::set_direct_hostname_addr_map( + std::map addr_map) { addr_map_ = std::move(addr_map); } +void ClientImpl::set_tunnel_hostname_addr_map( + std::map 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 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 addr_map) { + cli_->set_direct_hostname_addr_map(std::move(addr_map)); +} + +void Client::set_tunnel_hostname_addr_map( + std::map 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(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(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 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 guard(ctx_mutex_); - tls::set_verify_callback(ctx_, std::move(verifier)); + { + // Under the lock that creating a session from ctx_ takes + std::lock_guard guard(ctx_mutex_); + tls::set_verify_callback(ctx_, std::move(verifier)); + } + tls_settings_changed(); } void SSLClient::set_session_verifier( std::function 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 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(value); + + auto policy_value = + CFDictionaryGetValue(constraint, kSecTrustSettingsPolicy); + if (policy_value) { + auto policy = static_cast(const_cast(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( + CFDictionaryGetValue(constraint, kSecTrustSettingsResult)); + if (number) { + SInt32 number_value = 0; + if (!CFNumberGetValue(number, kCFNumberSInt32Type, &number_value)) { + continue; + } + result = static_cast(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 bool enumerate_macos_keychain_certs(Callback cb) { - bool loaded = false; const SecTrustSettingsDomain domains[] = { - kSecTrustSettingsDomainSystem, - kSecTrustSettingsDomainAdmin, kSecTrustSettingsDomainUser, + kSecTrustSettingsDomainAdmin, + kSecTrustSettingsDomainSystem, }; + std::set decided; + std::vector 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(CFDataGetLength(data)))) { - loaded = true; + if (!data) { continue; } + std::string der(reinterpret_cast(CFDataGetBytePtr(data)), + static_cast(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(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(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( - const_cast(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(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(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(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(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(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(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(-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(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(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(pem), - len + 1); // +1 for null terminator + auto ret = mbedtls_x509_crt_parse( + ca_chain, reinterpret_cast(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(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_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(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(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( 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 addr_map) { + set_direct_hostname_addr_map(std::move(addr_map)); +} + +void WebSocketClient::set_direct_hostname_addr_map( + std::map addr_map) { addr_map_ = std::move(addr_map); } +void WebSocketClient::set_tunnel_hostname_addr_map( + std::map 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 addr_map); + void + set_direct_hostname_addr_map(std::map addr_map); + void + set_tunnel_hostname_addr_map(std::map 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 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 addr_map_; + // Hostname to CONNECT target map, used only through a proxy. + std::map 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 addr_map); + void + set_direct_hostname_addr_map(std::map addr_map); + void + set_tunnel_hostname_addr_map(std::map 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 session_verifier_; // Like ca_cert_store_set_, keeps what ctx_ cannot report back: the verifier @@ -3768,6 +3816,8 @@ template 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 running_{false}; std::atomic 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 &duration); void set_interface(const std::string &intf); + [[deprecated("Use set_direct_hostname_addr_map() instead.")]] void set_hostname_addr_map(std::map addr_map); + void + set_direct_hostname_addr_map(std::map addr_map); + void + set_tunnel_hostname_addr_map(std::map 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 addr_map_; + // Hostname to CONNECT target map, used only through a proxy. + std::map 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;