mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-23 11:54:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bcfea7b90 | ||
|
|
fd71c11e0e | ||
|
|
fd5dd79bd2 | ||
|
|
6304fc6abb | ||
|
|
8772d10c90 | ||
|
|
98647e059d | ||
|
|
7a9cee0e05 | ||
|
|
71b5f0a1c8 | ||
|
|
67b88030e8 | ||
|
|
41c85c0733 |
No files matched your search
@@ -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()`:
|
||||
|
||||
- `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
|
||||
|
||||
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);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Peer Certificate Inspection
|
||||
@@ -404,6 +406,8 @@ ret = svr.remove_mount_point("/");
|
||||
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
|
||||
// User defined file extension and MIME type mappings
|
||||
svr.set_file_extension_and_mimetype_mapping("cc", "text/x-c");
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.62.0"
|
||||
version = "0.63.0"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -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.63.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003f00"
|
||||
|
||||
#ifdef _WIN32
|
||||
#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(),
|
||||
// since the store handle itself is owned by ctx_ and leaves no other trace.
|
||||
// Used to keep custom CA configuration exclusive with system CA loading.
|
||||
bool ca_cert_store_set_ = false;
|
||||
|
||||
// Set by set_ca_cert_store() and load_ca_cert_store() even when they bring
|
||||
// no certificate, so that a CA that failed to come through fails
|
||||
// verification instead of letting the system CAs in.
|
||||
bool custom_ca_loaded_ = false;
|
||||
|
||||
std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
|
||||
|
||||
// Like ca_cert_store_set_, keeps what ctx_ cannot report back: the verifier
|
||||
@@ -4793,6 +4797,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;
|
||||
@@ -5955,45 +5962,25 @@ inline bool is_valid_close_payload(const std::string &payload) {
|
||||
|
||||
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) {
|
||||
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; }
|
||||
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 == 1 && path[beg] == '.') ||
|
||||
(len == 2 && path[beg] == '.' && path[beg + 1] == '.')) {
|
||||
return false;
|
||||
}
|
||||
beg = i + 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11343,6 +11330,17 @@ inline bool setup_client_tls_session(
|
||||
return false;
|
||||
};
|
||||
|
||||
// A TLS failure, whose verification codes name the Error too
|
||||
auto fail_tls = [&](ErrorCode code, uint64_t backend_error) {
|
||||
auto error = Error::SSLConnection;
|
||||
if (code == ErrorCode::CertVerifyFailed) {
|
||||
error = Error::SSLServerVerification;
|
||||
} else if (code == ErrorCode::HostnameMismatch) {
|
||||
error = Error::SSLServerHostnameVerification;
|
||||
}
|
||||
return fail(error, static_cast<int>(code), backend_error);
|
||||
};
|
||||
|
||||
if (!ctx) {
|
||||
session = nullptr;
|
||||
return fail(Error::SSLConnection, 0, 0);
|
||||
@@ -11358,31 +11356,34 @@ inline bool setup_client_tls_session(
|
||||
!options.server_certificate_verifier_set;
|
||||
#endif
|
||||
|
||||
// The chain, and a host name, are verified during the handshake, so that a
|
||||
// server failing them never gets to see a client certificate. Unless
|
||||
// Schannel verifies the chain instead, this holds for IP hosts too, whose
|
||||
// identity is checked after the handshake by verify_hostname().
|
||||
auto verify_in_handshake =
|
||||
server_certificate_verification && !windows_verifies_chain;
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
// A session verifier may accept a certificate the built-in check rejects,
|
||||
// and runs only after the handshake
|
||||
verify_in_handshake = verify_in_handshake && !options.session_verifier;
|
||||
#endif
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> guard;
|
||||
if (options.ctx_mutex) {
|
||||
guard = std::unique_lock<std::mutex>(*options.ctx_mutex);
|
||||
}
|
||||
#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
|
||||
// Mbed TLS and wolfSSL need the verification mode set explicitly, on the
|
||||
// context, so under the same lock; OpenSSL uses SSL_VERIFY_NONE and does
|
||||
// all verification post-handshake. Unless Schannel verifies the chain
|
||||
// instead, chain verification happens during the handshake even for IP
|
||||
// hosts; the certificate identity is verified post-handshake via
|
||||
// verify_hostname().
|
||||
set_verify_client(ctx, server_certificate_verification &&
|
||||
!windows_verifies_chain);
|
||||
#endif
|
||||
// The mode is set on the context, so under the same lock
|
||||
set_verify_client(ctx, verify_in_handshake);
|
||||
session = create_session(ctx, sock);
|
||||
}
|
||||
if (!session) { return fail(Error::SSLConnection, 0, get_error()); }
|
||||
|
||||
// RFC 6066: SNI must not be set for IP addresses; skip it for IP hosts, so
|
||||
// their identity is checked post-handshake below instead. On Mbed TLS and
|
||||
// wolfSSL, set_sni also drives handshake-time hostname verification, so
|
||||
// options.server_hostname_verification is threaded through here.
|
||||
// their identity is checked post-handshake below instead.
|
||||
if (!is_ip_address(host)) {
|
||||
if (!set_sni(session, host.c_str(), options.server_hostname_verification)) {
|
||||
if (!set_sni(session, host.c_str(),
|
||||
verify_in_handshake && options.server_hostname_verification)) {
|
||||
return fail(Error::SSLConnection, 0, get_error());
|
||||
}
|
||||
}
|
||||
@@ -11390,13 +11391,7 @@ inline bool setup_client_tls_session(
|
||||
TlsError tls_err;
|
||||
if (!connect_nonblocking(session, sock, timeout_sec, timeout_usec,
|
||||
&tls_err)) {
|
||||
auto error = Error::SSLConnection;
|
||||
if (tls_err.code == ErrorCode::CertVerifyFailed) {
|
||||
error = Error::SSLServerVerification;
|
||||
} else if (tls_err.code == ErrorCode::HostnameMismatch) {
|
||||
error = Error::SSLServerHostnameVerification;
|
||||
}
|
||||
return fail(error, static_cast<int>(tls_err.code), tls_err.backend_code);
|
||||
return fail_tls(tls_err.code, tls_err.backend_code);
|
||||
}
|
||||
|
||||
auto verification_status = SSLVerifierResponse::NoDecisionMade;
|
||||
@@ -11413,14 +11408,14 @@ inline bool setup_client_tls_session(
|
||||
if (!windows_verifies_chain) {
|
||||
auto verify_result = get_verify_result(session);
|
||||
if (verify_result != 0) {
|
||||
return fail(Error::SSLServerVerification, 0,
|
||||
static_cast<uint64_t>(verify_result));
|
||||
return fail_tls(ErrorCode::CertVerifyFailed,
|
||||
static_cast<uint64_t>(verify_result));
|
||||
}
|
||||
}
|
||||
|
||||
auto server_cert = get_peer_cert(session);
|
||||
if (!server_cert) {
|
||||
return fail(Error::SSLServerVerification, 0, get_error());
|
||||
return fail_tls(ErrorCode::CertVerifyFailed, get_error());
|
||||
}
|
||||
auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); });
|
||||
|
||||
@@ -11429,8 +11424,7 @@ inline 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11445,7 +11439,7 @@ inline 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
|
||||
@@ -14330,6 +14324,8 @@ inline bool Server::read_content_core(
|
||||
}
|
||||
|
||||
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_) {
|
||||
// Prefix match, on a path segment boundary. A mount point of "/mount"
|
||||
// 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[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<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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
auto is_upgrade = detail::is_websocket_upgrade(req);
|
||||
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_ &&
|
||||
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);
|
||||
return write_response_with_content(strm, close_if_answered, req, res);
|
||||
}
|
||||
// Find matching WebSocket handler
|
||||
for (const auto &entry : websocket_handlers_) {
|
||||
@@ -15220,8 +15216,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
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 write_response_with_content(strm, close_if_answered, req, res);
|
||||
}
|
||||
|
||||
// Compute accept key
|
||||
@@ -15935,20 +15930,25 @@ inline 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(
|
||||
@@ -16371,7 +16371,7 @@ inline 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());
|
||||
}
|
||||
@@ -18815,8 +18815,10 @@ inline SSLServer::SSLServer(const PemMemory &pem) {
|
||||
last_ssl_error_ = static_cast<int>(get_error());
|
||||
free_context(ctx_);
|
||||
ctx_ = nullptr;
|
||||
} else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) {
|
||||
if (!load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) {
|
||||
} else if (pem.client_ca_pem) {
|
||||
auto ca_len = pem.client_ca_pem_len ? pem.client_ca_pem_len
|
||||
: strlen(pem.client_ca_pem);
|
||||
if (!load_ca_pem(ctx_, pem.client_ca_pem, ca_len)) {
|
||||
last_ssl_error_ = static_cast<int>(get_error());
|
||||
free_context(ctx_);
|
||||
ctx_ = nullptr;
|
||||
@@ -18944,7 +18946,9 @@ inline 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;
|
||||
}
|
||||
@@ -19212,6 +19216,7 @@ inline 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;
|
||||
}
|
||||
|
||||
inline void
|
||||
@@ -19244,6 +19249,7 @@ inline 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;
|
||||
}
|
||||
|
||||
inline bool SSLClient::load_certs() {
|
||||
@@ -19254,10 +19260,9 @@ inline bool SSLClient::load_certs() {
|
||||
std::call_once(initialize_cert_, [&]() {
|
||||
std::lock_guard<std::mutex> guard(ctx_mutex_);
|
||||
|
||||
ret = detail::load_client_ca_config(
|
||||
ctx_, ca_cert_file_path_, ca_cert_dir_path_,
|
||||
!ca_cert_pem_.empty() || ca_cert_store_set_, system_ca_mode_,
|
||||
last_backend_error_);
|
||||
ret = detail::load_client_ca_config(ctx_, ca_cert_file_path_,
|
||||
ca_cert_dir_path_, custom_ca_loaded_,
|
||||
system_ca_mode_, last_backend_error_);
|
||||
});
|
||||
|
||||
return ret;
|
||||
@@ -19283,7 +19288,7 @@ inline 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 =
|
||||
@@ -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
|
||||
// Returns a new STACK_OF(X509_NAME)* or nullptr on failure
|
||||
// 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 =
|
||||
(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
|
||||
@@ -20068,14 +20104,18 @@ inline void free_session(session_t session) {
|
||||
}
|
||||
|
||||
inline bool set_sni(session_t session, const char *hostname,
|
||||
bool /*verify_hostname*/) {
|
||||
bool verify_hostname) {
|
||||
if (!session || !hostname) return false;
|
||||
|
||||
auto ssl = static_cast<SSL *>(session);
|
||||
|
||||
// Set SNI (Server Name Indication) only - does not enable verification.
|
||||
// OpenSSL never binds identity checking to SNI (that happens post-
|
||||
// handshake in setup_client_tls_session()), so verify_hostname is unused.
|
||||
// The handshake's chain verification then checks the name as well
|
||||
if (verify_hostname &&
|
||||
X509_VERIFY_PARAM_set1_host(SSL_get0_param(ssl), hostname,
|
||||
strlen(hostname)) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(OPENSSL_IS_BORINGSSL)
|
||||
return SSL_set_tlsext_host_name(ssl, hostname) == 1;
|
||||
#else
|
||||
@@ -20095,9 +20135,7 @@ inline 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;
|
||||
}
|
||||
@@ -20153,10 +20191,7 @@ inline 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; }
|
||||
@@ -21456,6 +21491,9 @@ inline TlsError connect(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
|
||||
auto result = connect(session);
|
||||
|
||||
@@ -21463,7 +21501,6 @@ inline TlsError accept(session_t session) {
|
||||
if (result.code == ErrorCode::Success && session) {
|
||||
auto msession = static_cast<impl::MbedTlsSession *>(session);
|
||||
msession->sni_hostname = std::move(impl::mbedpending_sni());
|
||||
impl::mbedpending_sni().clear();
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -21508,6 +21545,9 @@ inline bool connect_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,
|
||||
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);
|
||||
@@ -21516,7 +21556,6 @@ inline bool accept_nonblocking(session_t session, socket_t sock,
|
||||
if (result && session) {
|
||||
auto msession = static_cast<impl::MbedTlsSession *>(session);
|
||||
msession->sni_hostname = std::move(impl::mbedpending_sni());
|
||||
impl::mbedpending_sni().clear();
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -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) {
|
||||
#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;
|
||||
if (!ca_chain) { return nullptr; }
|
||||
|
||||
@@ -22289,7 +22333,13 @@ inline 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;
|
||||
}
|
||||
|
||||
@@ -23966,18 +24016,18 @@ inline 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;
|
||||
}
|
||||
|
||||
inline 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;
|
||||
}
|
||||
|
||||
inline void
|
||||
|
||||
+285
-24
@@ -70,6 +70,7 @@ namespace tls {
|
||||
// live below the split border; the definitions are linked from httplib.cc.
|
||||
ca_store_t create_ca_store(const char *pem, size_t len);
|
||||
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
|
||||
std::vector<std::string> get_ca_names(ctx_t ctx);
|
||||
} // namespace tls
|
||||
} // namespace httplib
|
||||
#endif
|
||||
@@ -7417,9 +7418,7 @@ TEST_F(ServerTest, GetMethodDirTest) {
|
||||
TEST_F(ServerTest, GetMethodDirTestWithDoubleDots) {
|
||||
auto res = cli_.Get("/dir/../dir/test.html");
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ("text/html", res->get_header_value("Content-Type"));
|
||||
EXPECT_EQ("test.html", res->body);
|
||||
EXPECT_EQ(StatusCode::NotFound_404, res->status);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, GetMethodInvalidPath) {
|
||||
@@ -7451,9 +7450,7 @@ TEST_F(ServerTest, GetMethodDirMountTest) {
|
||||
TEST_F(ServerTest, GetMethodDirMountTestWithDoubleDots) {
|
||||
auto res = cli_.Get("/mount/dir/../dir/test.html");
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ("text/html", res->get_header_value("Content-Type"));
|
||||
EXPECT_EQ("test.html", res->body);
|
||||
EXPECT_EQ(StatusCode::NotFound_404, res->status);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, GetMethodInvalidMountPath) {
|
||||
@@ -7496,8 +7493,8 @@ TEST_F(ServerTest, GetMethodDirMountWithoutTrailingSlash) {
|
||||
TEST_F(ServerTest, GetMethodDirWithLeadingDoubleSlash) {
|
||||
auto res = cli_.Get("//dir");
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::MovedPermanently_301, res->status);
|
||||
EXPECT_EQ("./dir/", res->get_header_value("Location"));
|
||||
EXPECT_EQ(StatusCode::NotFound_404, res->status);
|
||||
EXPECT_FALSE(res->has_header("Location"));
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, PostMethod303) {
|
||||
@@ -12486,6 +12483,40 @@ TEST(MountTest, MultibytesPathName) {
|
||||
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
|
||||
// Issue #2435: mmap::open() must succeed even when another handle holds
|
||||
// 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
|
||||
|
||||
TEST(ClientProblemDetectionTest, ContentProvider) {
|
||||
@@ -15739,9 +15808,9 @@ TEST(SSLClientTest, ServerCertificateVerificationError_Online) {
|
||||
// OpenSSL: X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT (18)
|
||||
// Mbed TLS: MBEDTLS_X509_BADCERT_NOT_TRUSTED or similar flags
|
||||
EXPECT_NE(0UL, res.ssl_backend_error());
|
||||
EXPECT_EQ(static_cast<int>(tls::ErrorCode::CertVerifyFailed),
|
||||
res.ssl_error());
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
// For OpenSSL, ssl_error is 0 for verification errors
|
||||
EXPECT_EQ(0, res.ssl_error());
|
||||
#if !defined(_WIN32) || \
|
||||
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
|
||||
// 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
|
||||
EXPECT_NE(0UL, res.ssl_backend_error());
|
||||
EXPECT_EQ(static_cast<int>(tls::ErrorCode::HostnameMismatch),
|
||||
res.ssl_error());
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
// For OpenSSL, ssl_error is 0 for verification errors
|
||||
EXPECT_EQ(0, res.ssl_error());
|
||||
#if !defined(_WIN32) || \
|
||||
defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
|
||||
// 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) {
|
||||
ClientCertStillRequired([](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()));
|
||||
});
|
||||
ClientCertStillRequired(UpdateCertsPemWithClientCA);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
std::string server_cert_pem, server_key_pem, client_ca_pem;
|
||||
read_file(SERVER_CERT_FILE, server_cert_pem);
|
||||
@@ -16325,9 +16453,18 @@ TEST(SSLClientServerTest, PemMemoryClientCertMissing) {
|
||||
client_ca_pem.size(),
|
||||
nullptr // no password for server key
|
||||
};
|
||||
SSLServer svr(server_pem);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
RejectsClientWithoutCert(svr);
|
||||
for (auto client_ca_pem_len : {client_ca_pem.size(), size_t{0}}) {
|
||||
SCOPED_TRACE(client_ca_pem_len);
|
||||
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
|
||||
@@ -18070,6 +18207,37 @@ TEST(SSLClientTest, IpHostUntrustedChainFails) {
|
||||
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
|
||||
TEST(SSLClientTest, DisableSystemCa_Online) {
|
||||
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
|
||||
// response, is faulty framing, so the connection is not reused after it.
|
||||
TEST(ClientResponseSmugglingTest, FaultyTransferEncodingClosesConnection) {
|
||||
@@ -27701,6 +27906,62 @@ TEST(WebSocketPreRoutingTest, RunsOnceForUnmatchedUpgrade) {
|
||||
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) {
|
||||
Server svr;
|
||||
|
||||
|
||||
Reference in new issue
Block a user