Compare commits

..
10 Commits
Author SHA1 Message Date
yhirose 5bcfea7b90 Release v0.63.0 2026-10-21 22:26:15 -04:00
yhirose fd71c11e0e Verify the server certificate during the handshake on OpenSSL
The OpenSSL client finished the handshake with SSL_VERIFY_NONE and
checked the server certificate only afterwards, so it presented its
client certificate to a server that then failed verification. With a
server certificate verifier that accepted, wolfSSL did the same. Verify
the chain, and the host name, during the handshake on every backend,
keeping the built-in verdict when a verifier runs alongside it. On
OpenSSL a session verifier, which may accept a certificate the built-in
check rejects, still decides after the handshake.

A certificate that fails the built-in verification now reports
CertVerifyFailed, or HostnameMismatch for its name, in ssl_error() on
every backend, where OpenSSL left it 0.
2026-10-21 22:13:22 -04:00
yhirose fd5dd79bd2 Ready PSA Crypto before parsing a CA store on Mbed TLS 4.x
tls::create_ca_store() parses certificates, which on Mbed TLS 4.x needs
PSA Crypto. A context readies it when it is created, but a store can be
made before any context exists, and then a valid PEM failed to parse
and the call returned NULL. Ready PSA Crypto first, as the contexts do.
2026-10-21 17:16:11 -04:00
yhirose 6304fc6abb Keep the system CAs out when a custom CA comes through empty
Under the default system CA policy, a custom CA given to a client keeps
the system CAs out. SSLClient and WebSocketClient only noted the custom
CA when set_ca_cert_store() got a store or load_ca_cert_store() got some
bytes, so a NULL store, such as the one tls::create_ca_store() returns
for a PEM it cannot parse, or an empty PEM left the client trusting
every system CA instead. Note the custom CA on every call, so that such
a client fails verification. A redirect still passes a CA on only when
the original client had one.
2026-10-21 17:11:52 -04:00
yhirose 8772d10c90 Serve only canonical paths from mount points
A mount point left ".", ".." and empty path segments for the file
system to resolve, while the pre-routing handler, where the README has
applications guard static files, saw the path as it was sent. A file
could then be reached under a spelling that a check on req.path did not
match. Serve a path from a mount point only when it has none of those
segments other than a trailing '/', so that the file served is the one
req.path names; any other path now gets 404. The README also notes the
other names a file system can give a file.
2026-10-21 16:55:43 -04:00
yhirose 98647e059d Require client certificates once update_certs_pem() loads a client CA
SSLServer::update_certs_pem() loaded the client CA it was given but
left client certificate verification as it was, so on a server built
without a client CA it returned true while clients without a
certificate still got in. Turn verification on, as the constructors do
when they are given a client CA. A client CA passed to
update_certs_pem() now always makes client certificates required.
2026-10-21 16:41:12 -04:00
yhirose 7a9cee0e05 Drop the SNI of a failed handshake on the Mbed TLS server
The Mbed TLS server keeps the SNI from the ClientHello in a
thread-local until the handshake succeeds, and its SNI callback only
runs for a ClientHello that carries one. A handshake that failed left
its SNI behind, so the next connection served by the same thread
without SNI reported the earlier client's name in req.sni(). Start
each handshake with the thread-local cleared.
2026-10-21 16:29:56 -04:00
yhirose 71b5f0a1c8 Read a NUL-terminated client CA in SSLServer::PemMemory
SSLServer::PemMemory takes a length of 0 to mean a NUL-terminated
string for cert_pem and key_pem, but skipped client_ca_pem when its
length was 0, so a server given its client CA that way started without
requiring client certificates. Read the client CA under the same rule
as the other fields. An empty client CA now fails to load instead of
turning the check off, and the README documents the length-0 rule.
2026-10-21 16:19:15 -04:00
metsw24-max 67b88030e8 End the stream at the header section for HEAD, 204 and 304 in open_stream (#2636)
ClientImpl::open_stream built the body reader from Content-Length and Transfer-Encoding whatever the method or status, so a stream on a response that RFC 9112 6.3 ends at its header section was read on: with a keep-alive server read() blocked until the read timeout, and when the server closed it reported a read error for a complete response. stream::Head() against cpp-httplib's own server, which puts the GET body's Content-Length on a HEAD response, hit it every time.

open_stream_() now returns with the body reader at its end for a response to HEAD and for a 204 or 304, as the buffered path skips the body for them. Content-Encoding is skipped too, so a coding the build lacks no longer fails such a stream.
2026-10-20 17:31:45 -04:00
metsw24-max 41c85c0733 Close the connection on an answered upgrade body in process_request (#2637)
When pre_routing_handler or pre_request_handler answers a WebSocket upgrade request, Server::process_request returns before the drain of an unread body and closes the connection only for a 4xx/5xx. A 2xx/3xx answer to an upgrade request with a framed body left that body to be parsed as the next request on the keep-alive connection. Those two early returns now close the connection when the request has a framed body, as the Expect rejection already does.
2026-10-20 17:08:21 -04:00
4 changed files with 509 additions and 194 deletions

No files matched your search

+5 -1
View File
@@ -108,7 +108,7 @@ cli.enable_server_hostname_verification(false);
When SSL operations fail, cpp-httplib provides detailed error information through `ssl_error()` and `ssl_backend_error()`: When SSL operations fail, cpp-httplib provides detailed error information through `ssl_error()` and `ssl_backend_error()`:
- `ssl_error()` - Returns the TLS-level error as a backend-independent `httplib::tls::ErrorCode` value (e.g., `Fatal`, `CertVerifyFailed`), cast to `int`. `CertVerifyFailed` comes from Mbed TLS and wolfSSL. OpenSSL checks the chain and the host name after the handshake, so a failure there leaves this `0` - `ssl_error()` - Returns the TLS-level error as a backend-independent `httplib::tls::ErrorCode` value (e.g., `Fatal`, `CertVerifyFailed`), cast to `int`. A server certificate that the built-in verification rejects reports `CertVerifyFailed`, or `HostnameMismatch` when only its name does not match
- `ssl_backend_error()` - Returns the backend-specific error code (e.g., `ERR_get_error()` for OpenSSL/wolfSSL, return value for Mbed TLS). With OpenSSL, a certificate verification failure reports the verify result (`X509_V_ERR_*`) here instead. On Windows, when CryptoAPI verifies the chain, a failure there reports the CryptoAPI error code here - `ssl_backend_error()` - Returns the backend-specific error code (e.g., `ERR_get_error()` for OpenSSL/wolfSSL, return value for Mbed TLS). With OpenSSL, a certificate verification failure reports the verify result (`X509_V_ERR_*`) here instead. On Windows, when CryptoAPI verifies the chain, a failure there reports the CryptoAPI error code here
A request that fails after the handshake with `Error::Read` or `Error::Write` also carries the TLS error behind it in both values, such as a TLS 1.3 "certificate required" alert from the server. Both values are reset for each request. A request that fails after the handshake with `Error::Read` or `Error::Write` also carries the TLS error behind it in both values, such as a TLS 1.3 "certificate required" alert from the server. Both values are reset for each request.
@@ -210,6 +210,8 @@ client_pem.key_pem_len = client_key.size();
httplib::SSLClient cli("api.example.com", 443, client_pem); httplib::SSLClient cli("api.example.com", 443, client_pem);
``` ```
A length of 0 means the field is a NUL-terminated string.
`httplib::ws::WebSocketClient` has the same `PemMemory` constructor for `wss://` connections. See [README-websocket.md](README-websocket.md) for details. `httplib::ws::WebSocketClient` has the same `PemMemory` constructor for `wss://` connections. See [README-websocket.md](README-websocket.md) for details.
### Peer Certificate Inspection ### Peer Certificate Inspection
@@ -404,6 +406,8 @@ ret = svr.remove_mount_point("/");
ret = svr.remove_mount_point("/public"); ret = svr.remove_mount_point("/public");
``` ```
A mount point serves a path only when it has no `.`, `..` or empty segment other than a trailing `/`, so a check on `req.path` in `set_pre_routing_handler()` sees the path of the file that is served. A path ending in `/`, and the mount point itself such as `/public`, serve the `index.html` in that directory. The file system can still give a file other names: another case on a case-insensitive file system, such as the default ones on macOS and Windows, and on Windows a name with a trailing `.` or space, or an 8.3 short name such as `PRIVAT~1`. Compare paths without regard to case on such a file system, and on Windows keep files that need a guard out of mounted directories.
```cpp ```cpp
// User defined file extension and MIME type mappings // User defined file extension and MIME type mappings
svr.set_file_extension_and_mimetype_mapping("cc", "text/x-c"); svr.set_file_extension_and_mimetype_mapping("cc", "text/x-c");
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site] [site]
title = "cpp-httplib" title = "cpp-httplib"
version = "0.62.0" version = "0.63.0"
hostname = "https://yhirose.github.io" hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib" base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved." footer_message = "© 2026 Yuji Hirose. All rights reserved."
+218 -168
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H #ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.62.0" #define CPPHTTPLIB_VERSION "0.63.0"
#define CPPHTTPLIB_VERSION_NUM "0x003e00" #define CPPHTTPLIB_VERSION_NUM "0x003f00"
#ifdef _WIN32 #ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -3527,9 +3527,13 @@ private:
// Tracks whether a custom CA store was applied via set_ca_cert_store(), // 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. // 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; 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_; std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
// Like ca_cert_store_set_, keeps what ctx_ cannot report back: the verifier // Like ca_cert_store_set_, keeps what ctx_ cannot report back: the verifier
@@ -4793,6 +4797,9 @@ private:
tls::session_t tls_session_ = nullptr; tls::session_t tls_session_ = nullptr;
std::string ca_cert_file_path_; std::string ca_cert_file_path_;
std::string ca_cert_dir_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 custom_ca_loaded_ = false;
bool certs_loaded_ = false; bool certs_loaded_ = false;
SystemCAMode system_ca_mode_ = SystemCAMode::Auto; SystemCAMode system_ca_mode_ = SystemCAMode::Auto;
@@ -5955,45 +5962,25 @@ inline bool is_valid_close_payload(const std::string &payload) {
namespace detail { namespace detail {
// A path a mount point serves: absolute, with no ".", ".." or empty segment
// except a trailing '/', so the file served is the one that req.path names to
// the handlers that ran before it.
inline bool is_valid_path(const std::string &path) { inline bool is_valid_path(const std::string &path) {
size_t level = 0; if (path.empty() || path[0] != '/') { return false; }
size_t i = 0;
// Skip slash auto beg = size_t{1};
while (i < path.size() && path[i] == '/') { for (auto i = beg; i <= path.size(); i++) {
i++; if (i < path.size() && path[i] != '/') {
} if (path[i] == '\0' || path[i] == '\\') { return false; }
continue;
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 len = i - beg; auto len = i - beg;
assert(len > 0); if ((len == 0 && i < path.size()) || (len == 1 && path[beg] == '.') ||
(len == 2 && path[beg] == '.' && path[beg + 1] == '.')) {
if (!path.compare(beg, len, ".")) { return false;
;
} else if (!path.compare(beg, len, "..")) {
if (level == 0) { return false; }
level--;
} else {
level++;
}
// Skip slash
while (i < path.size() && path[i] == '/') {
i++;
} }
beg = i + 1;
} }
return true; return true;
} }
@@ -11343,6 +11330,17 @@ inline bool setup_client_tls_session(
return false; 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) { if (!ctx) {
session = nullptr; session = nullptr;
return fail(Error::SSLConnection, 0, 0); return fail(Error::SSLConnection, 0, 0);
@@ -11358,31 +11356,34 @@ inline bool setup_client_tls_session(
!options.server_certificate_verifier_set; !options.server_certificate_verifier_set;
#endif #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; std::unique_lock<std::mutex> guard;
if (options.ctx_mutex) { if (options.ctx_mutex) {
guard = std::unique_lock<std::mutex>(*options.ctx_mutex); guard = std::unique_lock<std::mutex>(*options.ctx_mutex);
} }
#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT) // The mode is set on the context, so under the same lock
// Mbed TLS and wolfSSL need the verification mode set explicitly, on the set_verify_client(ctx, verify_in_handshake);
// 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
session = create_session(ctx, sock); session = create_session(ctx, sock);
} }
if (!session) { return fail(Error::SSLConnection, 0, get_error()); } 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 // 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 // their identity is checked post-handshake below instead.
// wolfSSL, set_sni also drives handshake-time hostname verification, so
// options.server_hostname_verification is threaded through here.
if (!is_ip_address(host)) { 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()); return fail(Error::SSLConnection, 0, get_error());
} }
} }
@@ -11390,13 +11391,7 @@ inline bool setup_client_tls_session(
TlsError tls_err; TlsError tls_err;
if (!connect_nonblocking(session, sock, timeout_sec, timeout_usec, if (!connect_nonblocking(session, sock, timeout_sec, timeout_usec,
&tls_err)) { &tls_err)) {
auto error = Error::SSLConnection; return fail_tls(tls_err.code, tls_err.backend_code);
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);
} }
auto verification_status = SSLVerifierResponse::NoDecisionMade; auto verification_status = SSLVerifierResponse::NoDecisionMade;
@@ -11413,14 +11408,14 @@ inline bool setup_client_tls_session(
if (!windows_verifies_chain) { if (!windows_verifies_chain) {
auto verify_result = get_verify_result(session); auto verify_result = get_verify_result(session);
if (verify_result != 0) { if (verify_result != 0) {
return fail(Error::SSLServerVerification, 0, return fail_tls(ErrorCode::CertVerifyFailed,
static_cast<uint64_t>(verify_result)); static_cast<uint64_t>(verify_result));
} }
} }
auto server_cert = get_peer_cert(session); auto server_cert = get_peer_cert(session);
if (!server_cert) { 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); }); auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); });
@@ -11429,8 +11424,7 @@ inline bool setup_client_tls_session(
// hostname is bound during the handshake. // hostname is bound during the handshake.
if (options.server_hostname_verification) { if (options.server_hostname_verification) {
if (!verify_hostname(server_cert, host.c_str())) { if (!verify_hostname(server_cert, host.c_str())) {
return fail(Error::SSLServerHostnameVerification, 0, return fail_tls(ErrorCode::HostnameMismatch, hostname_mismatch_code());
hostname_mismatch_code());
} }
} }
@@ -11445,7 +11439,7 @@ inline bool setup_client_tls_session(
!verify_cert_with_windows_schannel( !verify_cert_with_windows_schannel(
der, host, options.server_hostname_verification, wincrypt_error, der, host, options.server_hostname_verification, wincrypt_error,
session)) { session)) {
return fail(Error::SSLServerVerification, 0, wincrypt_error); return fail_tls(ErrorCode::CertVerifyFailed, wincrypt_error);
} }
} }
#endif #endif
@@ -14330,6 +14324,8 @@ inline bool Server::read_content_core(
} }
inline bool Server::handle_file_request(Request &req, Response &res) { inline bool Server::handle_file_request(Request &req, Response &res) {
if (!detail::is_valid_path(req.path)) { return false; }
for (const auto &entry : base_dirs_) { for (const auto &entry : base_dirs_) {
// Prefix match, on a path segment boundary. A mount point of "/mount" // Prefix match, on a path segment boundary. A mount point of "/mount"
// covers "/mount" and "/mount/...", but must not swallow "/mountdir/...". // covers "/mount" and "/mount/...", but must not swallow "/mountdir/...".
@@ -14340,85 +14336,80 @@ inline bool Server::handle_file_request(Request &req, Response &res) {
req.path.size() == entry.mount_point.size() || req.path.size() == entry.mount_point.size() ||
req.path[entry.mount_point.size()] == '/')) { req.path[entry.mount_point.size()] == '/')) {
std::string sub_path = "/" + req.path.substr(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;
auto path = entry.base_dir + sub_path; if (path.back() == '/') { path += "index.html"; }
if (path.back() == '/') { path += "index.html"; }
// Defense-in-depth: is_valid_path blocks ".." traversal in the URL, // Defense-in-depth: is_valid_path blocks ".." traversal in the URL,
// but symlinks/junctions can still escape the base directory. A path // but symlinks/junctions can still escape the base directory. A path
// that cannot be resolved, which includes one that does not exist, is // that cannot be resolved, which includes one that does not exist, is
// not served from here. // not served from here.
std::string resolved_path; std::string resolved_path;
if (!detail::canonicalize_path(path.c_str(), resolved_path)) { if (!detail::canonicalize_path(path.c_str(), resolved_path)) { continue; }
continue; if (!detail::is_path_within_base(resolved_path,
} entry.resolved_base_dir)) {
if (!detail::is_path_within_base(resolved_path, res.status = StatusCode::Forbidden_403;
entry.resolved_base_dir)) { return true;
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); auto content_type = detail::find_content_type(
path, file_extension_and_mimetype_map_, default_file_mimetype_);
if (stat.is_dir()) { auto encoding = detail::EncodingType::None;
// Relative, so "//dir" cannot name another host; "./" keeps a ':' if (static_file_compression_) {
// in the name from reading as a scheme. encoding = static_file_encoding(req, res, content_type, stat.size());
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()) { // The ETag names the representation actually sent, so a client that
for (const auto &kv : entry.headers) { // cached the compressed form revalidates against the compressed ETag
res.set_header(kv.first, kv.second); // 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( auto mtime = stat.mtime();
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());
}
// The ETag names the representation actually sent, so a client that auto last_modified = detail::file_mtime_to_http_date(mtime);
// cached the compressed form revalidates against the compressed ETag if (!last_modified.empty()) {
// and still gets a 304, while one that took identity keeps the plain res.set_header("Last-Modified", last_modified);
// 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(); if (check_if_not_modified(req, res, etag, mtime)) { return true; }
auto last_modified = detail::file_mtime_to_http_date(mtime); check_if_range(req, etag, mtime);
if (!last_modified.empty()) {
res.set_header("Last-Modified", last_modified);
}
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()) {
check_if_range(req, etag, mtime);
auto mm = std::make_shared<detail::mmap>(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); 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);
} }
} }
} }
@@ -15208,10 +15199,15 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
// An upgrade request has been through pre-routing before it reaches routing() // An upgrade request has been through pre-routing before it reaches routing()
auto is_upgrade = detail::is_websocket_upgrade(req); auto is_upgrade = detail::is_websocket_upgrade(req);
if (is_upgrade) { if (is_upgrade) {
// 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);
if (pre_routing_handler_ && if (pre_routing_handler_ &&
pre_routing_handler_(req, res) == HandlerResponse::Handled) { pre_routing_handler_(req, res) == HandlerResponse::Handled) {
if (res.status == -1) { res.status = StatusCode::OK_200; } if (res.status == -1) { res.status = StatusCode::OK_200; }
return write_response_with_content(strm, close_after_response, req, res); return write_response_with_content(strm, close_if_answered, req, res);
} }
// Find matching WebSocket handler // Find matching WebSocket handler
for (const auto &entry : websocket_handlers_) { for (const auto &entry : websocket_handlers_) {
@@ -15220,8 +15216,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
if (pre_request_handler_ && if (pre_request_handler_ &&
pre_request_handler_(req, res) == HandlerResponse::Handled) { pre_request_handler_(req, res) == HandlerResponse::Handled) {
if (res.status == -1) { res.status = StatusCode::OK_200; } if (res.status == -1) { res.status = StatusCode::OK_200; }
return write_response_with_content(strm, close_after_response, req, return write_response_with_content(strm, close_if_answered, req, res);
res);
} }
// Compute accept key // Compute accept key
@@ -15935,20 +15930,25 @@ inline ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
return handle; return handle;
} }
// Same framing check as ClientImpl::process_request(). A HEAD or bodyless handle.body_reader_.stream = handle.stream_;
// (204/304) response legitimately carries framing headers with no body. handle.body_reader_.payload_max_length = payload_max_length_;
if (req.method != "HEAD" &&
handle.response->status != StatusCode::NoContent_204 && // RFC 9112 6.3: a response to HEAD, and a 204 or 304 response, ends with its
handle.response->status != StatusCode::NotModified_304 && // header section, so none of its framing or coding headers apply.
detail::has_conflicting_content_length(handle.response->headers)) { 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.error = Error::Read;
handle.response.reset(); handle.response.reset();
return handle; return handle;
} }
handle.body_reader_.stream = handle.stream_;
handle.body_reader_.payload_max_length = payload_max_length_;
if (handle.response->has_header("Content-Length")) { if (handle.response->has_header("Content-Length")) {
bool is_invalid = false; bool is_invalid = false;
auto content_length = detail::get_header_value_u64( auto content_length = detail::get_header_value_u64(
@@ -16371,7 +16371,7 @@ inline bool ClientImpl::create_redirect_client(
} }
redirect_client.load_ca_cert_store(pem.data(), pem.size()); redirect_client.load_ca_cert_store(pem.data(), pem.size());
redirect_client.ca_cert_store_set_ = true; 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(), redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(),
ca_cert_pem_.size()); ca_cert_pem_.size());
} }
@@ -18815,8 +18815,10 @@ inline SSLServer::SSLServer(const PemMemory &pem) {
last_ssl_error_ = static_cast<int>(get_error()); last_ssl_error_ = static_cast<int>(get_error());
free_context(ctx_); free_context(ctx_);
ctx_ = nullptr; ctx_ = nullptr;
} else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) { } else if (pem.client_ca_pem) {
if (!load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) { 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()); last_ssl_error_ = static_cast<int>(get_error());
free_context(ctx_); free_context(ctx_);
ctx_ = nullptr; ctx_ = nullptr;
@@ -18944,7 +18946,9 @@ inline bool SSLServer::update_certs_pem(const char *cert_pem,
return false; return false;
} }
if (client_ca_pem) { 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; return true;
} }
@@ -19212,6 +19216,7 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
} else if (ca_cert_store) { } else if (ca_cert_store) {
tls::free_ca_store(ca_cert_store); tls::free_ca_store(ca_cert_store);
} }
custom_ca_loaded_ = true;
} }
inline void inline void
@@ -19244,6 +19249,7 @@ inline void SSLClient::load_ca_cert_store(const char *ca_cert,
ca_cert_pem_.append(ca_cert, size); ca_cert_pem_.append(ca_cert, size);
tls::load_ca_pem(ctx_, ca_cert, size); tls::load_ca_pem(ctx_, ca_cert, size);
} }
custom_ca_loaded_ = true;
} }
inline bool SSLClient::load_certs() { inline bool SSLClient::load_certs() {
@@ -19254,10 +19260,9 @@ inline bool SSLClient::load_certs() {
std::call_once(initialize_cert_, [&]() { std::call_once(initialize_cert_, [&]() {
std::lock_guard<std::mutex> guard(ctx_mutex_); std::lock_guard<std::mutex> guard(ctx_mutex_);
ret = detail::load_client_ca_config( ret = detail::load_client_ca_config(ctx_, ca_cert_file_path_,
ctx_, ca_cert_file_path_, ca_cert_dir_path_, ca_cert_dir_path_, custom_ca_loaded_,
!ca_cert_pem_.empty() || ca_cert_store_set_, system_ca_mode_, system_ca_mode_, last_backend_error_);
last_backend_error_);
}); });
return ret; return ret;
@@ -19283,7 +19288,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
options.windows_cert_verification = options.windows_cert_verification =
enable_windows_cert_verification_ && enable_windows_cert_verification_ &&
system_ca_mode_ != SystemCAMode::Disabled && ca_cert_file_path_.empty() && 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 // Only a verifier set through set_server_certificate_verifier() is seen
// here, not one installed with tls::set_verify_callback() directly. // here, not one installed with tls::set_verify_callback() directly.
options.server_certificate_verifier_set = options.server_certificate_verifier_set =
@@ -19717,6 +19722,30 @@ inline 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.
inline 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 // Helper: Create client CA list from PEM string
// Returns a new STACK_OF(X509_NAME)* or nullptr on failure // Returns a new STACK_OF(X509_NAME)* or nullptr on failure
// Caller takes ownership of returned list // Caller takes ownership of returned list
@@ -19792,7 +19821,14 @@ inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
verify_ctx.error_string = verify_ctx.error_string =
(error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr; (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 // Sets the verify mode from whether the peer's certificate is required and
@@ -20068,14 +20104,18 @@ inline void free_session(session_t session) {
} }
inline bool set_sni(session_t session, const char *hostname, inline bool set_sni(session_t session, const char *hostname,
bool /*verify_hostname*/) { bool verify_hostname) {
if (!session || !hostname) return false; if (!session || !hostname) return false;
auto ssl = static_cast<SSL *>(session); auto ssl = static_cast<SSL *>(session);
// Set SNI (Server Name Indication) only - does not enable verification. // The handshake's chain verification then checks the name as well
// OpenSSL never binds identity checking to SNI (that happens post- if (verify_hostname &&
// handshake in setup_client_tls_session()), so verify_hostname is unused. X509_VERIFY_PARAM_set1_host(SSL_get0_param(ssl), hostname,
strlen(hostname)) != 1) {
return false;
}
#if defined(OPENSSL_IS_BORINGSSL) #if defined(OPENSSL_IS_BORINGSSL)
return SSL_set_tlsext_host_name(ssl, hostname) == 1; return SSL_set_tlsext_host_name(ssl, hostname) == 1;
#else #else
@@ -20095,9 +20135,7 @@ inline TlsError connect(session_t session) {
if (ret == 1) { if (ret == 1) {
err.code = ErrorCode::Success; err.code = ErrorCode::Success;
} else { } else {
auto ssl_err = SSL_get_error(ssl, ret); impl::fill_connect_error(ssl, ret, err);
err.code = impl::map_ssl_error(ssl_err, err.sys_errno);
err.backend_code = ERR_get_error();
} }
return err; return err;
} }
@@ -20153,10 +20191,7 @@ inline bool connect_nonblocking(session_t session, socket_t sock,
break; break;
default: break; default: break;
} }
if (err) { if (err) { impl::fill_connect_error(ssl, res, *err); }
err->code = impl::map_ssl_error(ssl_err, err->sys_errno);
err->backend_code = ERR_get_error();
}
return false; return false;
} }
if (err) { err->code = ErrorCode::Success; } if (err) { err->code = ErrorCode::Success; }
@@ -21456,6 +21491,9 @@ inline TlsError connect(session_t session) {
} }
inline TlsError accept(session_t session) { inline 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 // Same as connect for Mbed TLS - handshake works for both client and server
auto result = connect(session); auto result = connect(session);
@@ -21463,7 +21501,6 @@ inline TlsError accept(session_t session) {
if (result.code == ErrorCode::Success && session) { if (result.code == ErrorCode::Success && session) {
auto msession = static_cast<impl::MbedTlsSession *>(session); auto msession = static_cast<impl::MbedTlsSession *>(session);
msession->sni_hostname = std::move(impl::mbedpending_sni()); msession->sni_hostname = std::move(impl::mbedpending_sni());
impl::mbedpending_sni().clear();
} }
return result; return result;
@@ -21508,6 +21545,9 @@ inline bool connect_nonblocking(session_t session, socket_t sock,
inline bool accept_nonblocking(session_t session, socket_t sock, inline bool accept_nonblocking(session_t session, socket_t sock,
time_t timeout_sec, time_t timeout_usec, time_t timeout_sec, time_t timeout_usec,
TlsError *err) { TlsError *err) {
// A failed handshake leaves its SNI behind, so start without it
impl::mbedpending_sni().clear();
// Same implementation as connect for Mbed TLS // Same implementation as connect for Mbed TLS
bool result = bool result =
connect_nonblocking(session, sock, timeout_sec, timeout_usec, err); connect_nonblocking(session, sock, timeout_sec, timeout_usec, err);
@@ -21516,7 +21556,6 @@ inline bool accept_nonblocking(session_t session, socket_t sock,
if (result && session) { if (result && session) {
auto msession = static_cast<impl::MbedTlsSession *>(session); auto msession = static_cast<impl::MbedTlsSession *>(session);
msession->sni_hostname = std::move(impl::mbedpending_sni()); msession->sni_hostname = std::move(impl::mbedpending_sni());
impl::mbedpending_sni().clear();
} }
return result; return result;
@@ -21922,6 +21961,11 @@ inline std::string error_string(uint64_t code) {
} }
inline ca_store_t create_ca_store(const char *pem, size_t len) { inline 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
auto *ca_chain = new (std::nothrow) mbedtls_x509_crt; auto *ca_chain = new (std::nothrow) mbedtls_x509_crt;
if (!ca_chain) { return nullptr; } if (!ca_chain) { return nullptr; }
@@ -22289,7 +22333,13 @@ inline int wolfssl_verify_callback(int preverify_ok,
verify_ctx.error_string = nullptr; 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; return accepted ? 1 : 0;
} }
@@ -23966,18 +24016,18 @@ inline void WebSocketClient::set_ca_cert_store(tls::ca_store_t store) {
if (store && tls_ctx_) { if (store && tls_ctx_) {
// set_ca_store takes ownership of store // set_ca_store takes ownership of store
tls::set_ca_store(tls_ctx_, store); tls::set_ca_store(tls_ctx_, store);
custom_ca_loaded_ = true;
} else if (store) { } else if (store) {
tls::free_ca_store(store); tls::free_ca_store(store);
} }
custom_ca_loaded_ = true;
} }
inline void WebSocketClient::load_ca_cert_store(const char *ca_cert, inline void WebSocketClient::load_ca_cert_store(const char *ca_cert,
std::size_t size) { std::size_t size) {
if (tls_ctx_ && ca_cert && size > 0) { if (tls_ctx_ && ca_cert && size > 0) {
tls::load_ca_pem(tls_ctx_, ca_cert, size); tls::load_ca_pem(tls_ctx_, ca_cert, size);
custom_ca_loaded_ = true;
} }
custom_ca_loaded_ = true;
} }
inline void inline void
+285 -24
View File
@@ -70,6 +70,7 @@ namespace tls {
// live below the split border; the definitions are linked from httplib.cc. // live below the split border; the definitions are linked from httplib.cc.
ca_store_t create_ca_store(const char *pem, size_t len); ca_store_t create_ca_store(const char *pem, size_t len);
bool set_verify_callback(ctx_t ctx, VerifyCallback callback); bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
std::vector<std::string> get_ca_names(ctx_t ctx);
} // namespace tls } // namespace tls
} // namespace httplib } // namespace httplib
#endif #endif
@@ -7417,9 +7418,7 @@ TEST_F(ServerTest, GetMethodDirTest) {
TEST_F(ServerTest, GetMethodDirTestWithDoubleDots) { TEST_F(ServerTest, GetMethodDirTestWithDoubleDots) {
auto res = cli_.Get("/dir/../dir/test.html"); auto res = cli_.Get("/dir/../dir/test.html");
ASSERT_TRUE(res) << "Error: " << to_string(res.error()); ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status); EXPECT_EQ(StatusCode::NotFound_404, res->status);
EXPECT_EQ("text/html", res->get_header_value("Content-Type"));
EXPECT_EQ("test.html", res->body);
} }
TEST_F(ServerTest, GetMethodInvalidPath) { TEST_F(ServerTest, GetMethodInvalidPath) {
@@ -7451,9 +7450,7 @@ TEST_F(ServerTest, GetMethodDirMountTest) {
TEST_F(ServerTest, GetMethodDirMountTestWithDoubleDots) { TEST_F(ServerTest, GetMethodDirMountTestWithDoubleDots) {
auto res = cli_.Get("/mount/dir/../dir/test.html"); auto res = cli_.Get("/mount/dir/../dir/test.html");
ASSERT_TRUE(res) << "Error: " << to_string(res.error()); ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status); EXPECT_EQ(StatusCode::NotFound_404, res->status);
EXPECT_EQ("text/html", res->get_header_value("Content-Type"));
EXPECT_EQ("test.html", res->body);
} }
TEST_F(ServerTest, GetMethodInvalidMountPath) { TEST_F(ServerTest, GetMethodInvalidMountPath) {
@@ -7496,8 +7493,8 @@ TEST_F(ServerTest, GetMethodDirMountWithoutTrailingSlash) {
TEST_F(ServerTest, GetMethodDirWithLeadingDoubleSlash) { TEST_F(ServerTest, GetMethodDirWithLeadingDoubleSlash) {
auto res = cli_.Get("//dir"); auto res = cli_.Get("//dir");
ASSERT_TRUE(res) << "Error: " << to_string(res.error()); ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::MovedPermanently_301, res->status); EXPECT_EQ(StatusCode::NotFound_404, res->status);
EXPECT_EQ("./dir/", res->get_header_value("Location")); EXPECT_FALSE(res->has_header("Location"));
} }
TEST_F(ServerTest, PostMethod303) { TEST_F(ServerTest, PostMethod303) {
@@ -12486,6 +12483,40 @@ TEST(MountTest, MultibytesPathName) {
EXPECT_EQ(U8("日本語コンテンツ"), res->body); EXPECT_EQ(U8("日本語コンテンツ"), res->body);
} }
// A mount point serves no path with an empty, "." or ".." segment, so a guard
// in the pre-routing handler sees the path of every file that is served
TEST(MountTest, AliasedPathDoesNotPassPreRoutingGuard) {
Server svr;
ASSERT_TRUE(svr.set_mount_point("/", "./www"));
svr.set_pre_routing_handler([](const Request &req, Response &res) {
if (req.path == "/dir/test.html") {
res.status = StatusCode::Unauthorized_401;
return Server::HandlerResponse::Handled;
}
return Server::HandlerResponse::Unhandled;
});
auto port = svr.bind_to_any_port(HOST);
ASSERT_LT(0, port);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
for (auto path :
{"/./dir/test.html", "/dir/./test.html", "//dir/test.html",
"/dir//test.html", "/dir/../dir/test.html", "/%2e/dir/test.html"}) {
SCOPED_TRACE(path);
auto res = cli.Get(path);
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::NotFound_404, res->status);
}
}
#ifdef _WIN32 #ifdef _WIN32
// Issue #2435: mmap::open() must succeed even when another handle holds // Issue #2435: mmap::open() must succeed even when another handle holds
// the file open for writing (e.g. an active log file). // the file open for writing (e.g. an active log file).
@@ -12977,6 +13008,44 @@ TEST(SNI_AutoDetectionTest, SNI_Logic) {
} }
} }
} }
// The SNI of a failed handshake does not reach the next connection that the
// same worker thread serves
TEST(SNI_AutoDetectionTest, FailedHandshakeLeavesNoSNI) {
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
svr.new_task_queue = [] { return new ThreadPool(1); };
svr.Get("/sni", [](const Request &req, Response &res) {
res.set_content(req.sni(), "text/plain");
});
auto port = svr.bind_to_any_port("127.0.0.1");
ASSERT_LT(0, port);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
{
// Sends SNI, then rejects the server certificate, which the CA did not sign
SSLClient cli("localhost", port);
cli.set_hostname_addr_map({{"localhost", "127.0.0.1"}});
cli.set_ca_cert_path(CLIENT_CA_CERT_FILE);
auto res = cli.Get("/sni");
ASSERT_FALSE(res);
EXPECT_EQ(Error::SSLServerVerification, res.error());
}
// An IP host sends no SNI (RFC 6066)
SSLClient cli("127.0.0.1", port);
cli.enable_server_certificate_verification(false);
auto res = cli.Get("/sni");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("", res->body);
}
#endif #endif
TEST(ClientProblemDetectionTest, ContentProvider) { TEST(ClientProblemDetectionTest, ContentProvider) {
@@ -15739,9 +15808,9 @@ TEST(SSLClientTest, ServerCertificateVerificationError_Online) {
// OpenSSL: X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT (18) // OpenSSL: X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT (18)
// Mbed TLS: MBEDTLS_X509_BADCERT_NOT_TRUSTED or similar flags // Mbed TLS: MBEDTLS_X509_BADCERT_NOT_TRUSTED or similar flags
EXPECT_NE(0UL, res.ssl_backend_error()); EXPECT_NE(0UL, res.ssl_backend_error());
EXPECT_EQ(static_cast<int>(tls::ErrorCode::CertVerifyFailed),
res.ssl_error());
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
// For OpenSSL, ssl_error is 0 for verification errors
EXPECT_EQ(0, res.ssl_error());
#if !defined(_WIN32) || \ #if !defined(_WIN32) || \
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
// On non-Windows or when Windows Schannel is disabled, the error comes // On non-Windows or when Windows Schannel is disabled, the error comes
@@ -15765,9 +15834,9 @@ TEST(SSLClientTest, ServerHostnameVerificationError_Online) {
// Verify backend error is captured for hostname verification failure // Verify backend error is captured for hostname verification failure
EXPECT_NE(0UL, res.ssl_backend_error()); EXPECT_NE(0UL, res.ssl_backend_error());
EXPECT_EQ(static_cast<int>(tls::ErrorCode::HostnameMismatch),
res.ssl_error());
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
// For OpenSSL, ssl_error is 0 for verification errors
EXPECT_EQ(0, res.ssl_error());
#if !defined(_WIN32) || \ #if !defined(_WIN32) || \
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
// On non-Windows or when Windows Schannel is disabled, the error comes // On non-Windows or when Windows Schannel is disabled, the error comes
@@ -16183,15 +16252,73 @@ TEST(SSLClientServerTest, ClientCertStillRequiredAfterVerifyCallbackCleared) {
}); });
} }
// The client verifies the server certificate during the handshake, so a
// server that fails the check never gets the client certificate, even when a
// verifier of the client's own accepts it
TEST(SSLClientServerTest, ClientCertNotSentToUnverifiedServer) {
struct Case {
const char *trusted_ca;
const char *host;
Error error;
};
// cert2.pem names localhost: the first client does not trust it, and the
// second trusts it but asks for another name
for (const auto &c :
{Case{CLIENT_CA_CERT_FILE, "localhost", Error::SSLServerVerification},
Case{SERVER_CERT2_FILE, "example.com",
Error::SSLServerHostnameVerification}}) {
for (auto with_verifier : {false, true}) {
SCOPED_TRACE(std::string(c.host) + (with_verifier ? " +verifier" : ""));
SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE,
CLIENT_CA_CERT_FILE);
ASSERT_TRUE(svr.is_valid());
std::atomic<bool> got_client_cert{false};
ASSERT_TRUE(tls::set_verify_callback(svr.tls_context(),
[&](const tls::VerifyContext &) {
got_client_cert = true;
return true;
}));
auto port = svr.bind_to_any_port("127.0.0.1");
ASSERT_LT(0, port);
thread t = thread([&] { svr.listen_after_bind(); });
svr.wait_until_ready();
SSLClient cli(c.host, port, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
cli.set_hostname_addr_map({{c.host, "127.0.0.1"}});
cli.set_ca_cert_path(c.trusted_ca);
if (with_verifier) {
cli.set_server_certificate_verifier(
[](const tls::VerifyContext &) { return true; });
}
auto res = cli.Get("/");
EXPECT_FALSE(res);
EXPECT_EQ(c.error, res.error());
svr.stop();
t.join();
EXPECT_FALSE(got_client_cert);
}
}
}
void UpdateCertsPemWithClientCA(SSLServer &svr) {
std::string cert_pem, key_pem, ca_pem;
read_file(SERVER_CERT_FILE, cert_pem);
read_file(SERVER_PRIVATE_KEY_FILE, key_pem);
read_file(CLIENT_CA_CERT_FILE, ca_pem);
ASSERT_TRUE(
svr.update_certs_pem(cert_pem.c_str(), key_pem.c_str(), ca_pem.c_str()));
}
TEST(SSLClientServerTest, UpdateCertsPemWithClientCARequiresClientCert) {
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
UpdateCertsPemWithClientCA(svr);
RejectsClientWithoutCert(svr);
}
TEST(SSLClientServerTest, ClientCertStillRequiredAfterUpdateCertsPem) { TEST(SSLClientServerTest, ClientCertStillRequiredAfterUpdateCertsPem) {
ClientCertStillRequired([](SSLServer &svr) { ClientCertStillRequired(UpdateCertsPemWithClientCA);
std::string cert_pem, key_pem, ca_pem;
read_file(SERVER_CERT_FILE, cert_pem);
read_file(SERVER_PRIVATE_KEY_FILE, key_pem);
read_file(CLIENT_CA_CERT_FILE, ca_pem);
ASSERT_TRUE(svr.update_certs_pem(cert_pem.c_str(), key_pem.c_str(),
ca_pem.c_str()));
});
} }
// A TLS error from one request must not show up in a later request's Result // A TLS error from one request must not show up in a later request's Result
@@ -16309,7 +16436,8 @@ TEST(SSLClientServerTest, PemMemoryClientEncryptedCertPresent) {
CLIENT_ENCRYPTED_PRIVATE_KEY_PASS); CLIENT_ENCRYPTED_PRIVATE_KEY_PASS);
} }
// A server built from PEM memory with a client CA requires client certificates // A server built from PEM memory with a client CA requires client certificates,
// also when the CA is a NUL-terminated string passed with a length of 0
TEST(SSLClientServerTest, PemMemoryClientCertMissing) { TEST(SSLClientServerTest, PemMemoryClientCertMissing) {
std::string server_cert_pem, server_key_pem, client_ca_pem; std::string server_cert_pem, server_key_pem, client_ca_pem;
read_file(SERVER_CERT_FILE, server_cert_pem); read_file(SERVER_CERT_FILE, server_cert_pem);
@@ -16325,9 +16453,18 @@ TEST(SSLClientServerTest, PemMemoryClientCertMissing) {
client_ca_pem.size(), client_ca_pem.size(),
nullptr // no password for server key nullptr // no password for server key
}; };
SSLServer svr(server_pem); for (auto client_ca_pem_len : {client_ca_pem.size(), size_t{0}}) {
ASSERT_TRUE(svr.is_valid()); SCOPED_TRACE(client_ca_pem_len);
RejectsClientWithoutCert(svr); server_pem.client_ca_pem_len = client_ca_pem_len;
SSLServer svr(server_pem);
ASSERT_TRUE(svr.is_valid());
RejectsClientWithoutCert(svr);
}
// An empty CA fails to load instead of turning the check off
server_pem.client_ca_pem = "";
server_pem.client_ca_pem_len = 0;
EXPECT_FALSE(SSLServer(server_pem).is_valid());
} }
// The PemMemory lengths are honoured, so the PEM needs no NUL terminator // The PemMemory lengths are honoured, so the PEM needs no NUL terminator
@@ -18070,6 +18207,37 @@ TEST(SSLClientTest, IpHostUntrustedChainFails) {
ASSERT_FALSE(res); ASSERT_FALSE(res);
} }
#ifndef CPPHTTPLIB_WOLFSSL_SUPPORT
// A custom CA that brings no certificate still keeps the system CAs out, so a
// CA that failed to come through fails verification instead of trusting them.
// (wolfSSL does not list the system CAs it loaded.)
TEST(SSLClientTest, EmptyCustomCaKeepsSystemCasOut) {
SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
auto port = svr.bind_to_any_port("127.0.0.1");
ASSERT_LT(0, port);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
// The CAs a client trusts once its first request has loaded them
auto loaded_cas = [&](const std::function<void(Client &)> &setup) {
Client cli("https://127.0.0.1:" + std::to_string(port));
setup(cli);
cli.Get("/");
return tls::get_ca_names(cli.tls_context()).size();
};
EXPECT_EQ(0u, loaded_cas([](Client &cli) { cli.load_ca_cert_store("", 0); }));
EXPECT_EQ(0u,
loaded_cas([](Client &cli) { cli.set_ca_cert_store(nullptr); }));
}
#endif
// enable_system_ca(false) prevents system CA loading entirely // enable_system_ca(false) prevents system CA loading entirely
TEST(SSLClientTest, DisableSystemCa_Online) { TEST(SSLClientTest, DisableSystemCa_Online) {
SSLClient cli("google.com"); SSLClient cli("google.com");
@@ -23742,6 +23910,43 @@ TEST(ClientResponseSmugglingTest, BodylessResponseNotRejected) {
} }
} }
// RFC 9112 6.3: a response to HEAD, and a 204 or 304 response, ends with its
// header section, so a stream opened on one has nothing to read, whatever
// framing or coding headers it carries.
TEST(ClientResponseSmugglingTest, BodylessResponseStreamEndsAtOnce) {
struct Case {
const char *method;
const char *status;
};
// A reader of the declared body reports an early close when nothing
// follows, and hands back what does.
struct Framing {
const char *headers;
const char *after;
};
for (const auto &c : {Case{"HEAD", "200 OK"}, Case{"GET", "204 No Content"},
Case{"GET", "304 Not Modified"}}) {
for (const auto &f :
{Framing{"Content-Length: 5\r\n", ""},
Framing{"Content-Length: 5\r\n", "hello"},
Framing{"Transfer-Encoding: chunked\r\n", ""},
Framing{"Transfer-Encoding: chunked\r\n", "5\r\nhello\r\n0\r\n\r\n"},
// Not decoded, so a build without Brotli opens it too
Framing{"Content-Encoding: br\r\n", ""}}) {
auto response = std::string("HTTP/1.1 ") + c.status + "\r\n" + f.headers +
"Connection: close\r\n"
"\r\n" +
f.after;
with_single_response(response, [&](Client &cli) {
auto handle = cli.open_stream(c.method, "/");
ASSERT_TRUE(handle.is_valid()) << c.method << ' ' << response;
EXPECT_EQ("", read_all(handle)) << c.method << ' ' << response;
EXPECT_FALSE(handle.has_read_error()) << c.method << ' ' << response;
});
}
}
}
// RFC 9112 6.1: Transfer-Encoding next to a Content-Length, or in an HTTP/1.0 // RFC 9112 6.1: Transfer-Encoding next to a Content-Length, or in an HTTP/1.0
// response, is faulty framing, so the connection is not reused after it. // response, is faulty framing, so the connection is not reused after it.
TEST(ClientResponseSmugglingTest, FaultyTransferEncodingClosesConnection) { TEST(ClientResponseSmugglingTest, FaultyTransferEncodingClosesConnection) {
@@ -27701,6 +27906,62 @@ TEST(WebSocketPreRoutingTest, RunsOnceForUnmatchedUpgrade) {
EXPECT_EQ(1, pre_request_calls); EXPECT_EQ(1, pre_request_calls);
} }
// An upgrade request answered by a hook leaves its body unread, so that body
// must not be served as the next request on the connection.
TEST(WebSocketPreRoutingTest, AnsweredUpgradeBodyIsNotNextRequest) {
Server svr;
svr.set_pre_routing_handler([](const Request &req, Response &res) {
if (req.path == "/pre-routing") {
res.set_content("pre-routing", "text/plain");
return Server::HandlerResponse::Handled;
}
return Server::HandlerResponse::Unhandled;
});
svr.set_pre_request_handler([](const Request &req, Response &res) {
if (req.matched_route == "/pre-request") {
res.set_content("pre-request", "text/plain");
return Server::HandlerResponse::Handled;
}
return Server::HandlerResponse::Unhandled;
});
svr.Get("/smuggled", [](const Request &, Response &res) {
res.set_content("smuggled", "text/plain");
});
svr.WebSocket("/pre-request",
[](const Request &, ws::WebSocket &ws) { ws.send("ws"); });
auto port = svr.bind_to_any_port(HOST);
std::thread t([&]() { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
});
svr.wait_until_ready();
const std::string body = "GET /smuggled HTTP/1.1\r\nHost: localhost\r\n\r\n";
for (auto path : {"/pre-routing", "/pre-request"}) {
std::string req = std::string("GET ") + path +
" HTTP/1.1\r\n"
"Host: localhost\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"Sec-WebSocket-Version: 13\r\n"
"Content-Length: " +
std::to_string(body.size()) + "\r\n\r\n" + body;
std::string res;
ASSERT_TRUE(send_request(5, req, &res, port));
EXPECT_EQ(0u, res.find("HTTP/1.1 200 OK\r\n")) << path;
EXPECT_NE(std::string::npos, res.find("Connection: close\r\n")) << path;
// Only one response, and the body was not served as a request
EXPECT_EQ(std::string::npos, res.find("HTTP/1.1", 1)) << path;
}
}
TEST(WebSocketPreRequestTest, RejectWithoutAuth) { TEST(WebSocketPreRequestTest, RejectWithoutAuth) {
Server svr; Server svr;