// Session management
session_t create_session(ctx_t ctx, socket_t sock);
void free_session(session_t session);
-bool set_sni(session_t session, const char *hostname);
-bool set_hostname(session_t session, const char *hostname);
+bool set_sni(session_t session, const char *hostname, bool verify_hostname);
// Handshake (non-blocking capable)
TlsError connect(session_t session);
#endif
}
+// Half-closes the write side and drains any in-flight/queued bytes before
+// the final shutdown+close. Closing with unread data in the receive queue
+// (or bytes arriving after the receive side is closed) makes the stack send
+// an abortive RST instead of a graceful FIN, which can make the peer see the
+// response as a failed read even though it was fully written.
+void drain_and_close_socket(socket_t sock) noexcept {
+#ifdef _WIN32
+ shutdown(sock, SD_SEND);
+#else
+ shutdown(sock, SHUT_WR);
+#endif
+
+ char buf[CPPHTTPLIB_RECV_BUFSIZ];
+ size_t total = 0;
+ const auto deadline = std::chrono::steady_clock::now() +
+ std::chrono::milliseconds(100); // bound #1
+
+ while (total < size_t(1024u * 1024u)) { // bound #2
+ const auto remaining =
+ std::chrono::duration_cast<std::chrono::microseconds>(
+ deadline - std::chrono::steady_clock::now())
+ .count();
+ if (remaining <= 0) { break; }
+ if (select_read(sock, 0, static_cast<time_t>(remaining)) <= 0) { break; }
+ const auto n = read_socket(sock, buf, sizeof(buf), CPPHTTPLIB_RECV_FLAGS);
+ if (n <= 0) { break; }
+ total += static_cast<size_t>(n);
+ }
+
+ shutdown_socket(sock);
+ close_socket(sock);
+}
+
std::string escape_abstract_namespace_unix_domain(const std::string &s) {
if (s.size() > 1 && s[0] == '\0') {
auto ret = s;
return true;
}
+bool parse_status_line(const char *line, std::string &version,
+ int &status, std::string &reason) {
+#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+ thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n");
+#else
+ thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n");
+#endif
+
+ std::cmatch m;
+ if (!std::regex_match(line, m, re)) { return false; }
+ version = std::string(m[1]);
+ status = std::stoi(std::string(m[2]));
+ reason = std::string(m[3]);
+ return true;
+}
+
+// Everything WebSocketClient::connect() reports about the upgrade exchange.
+// status stays -1 until a status line is parsed, mirroring stream::Result.
+struct WebSocketUpgradeResponse {
+ Error error = Error::Success;
+ int status = -1;
+ Headers headers;
+ std::string selected_subprotocol;
+};
+
bool read_websocket_upgrade_response(Stream &strm,
const std::string &expected_accept,
- std::string &selected_subprotocol) {
+ WebSocketUpgradeResponse &upgrade) {
// Read status line
const auto bufsiz = 2048;
char buf[bufsiz];
stream_line_reader line_reader(strm, buf, bufsiz);
- if (!line_reader.getline()) { return false; }
+ if (!line_reader.getline()) {
+ upgrade.error = Error::Read;
+ return false;
+ }
+
+ std::string version;
+ std::string reason;
+ if (!parse_status_line(line_reader.ptr(), version, upgrade.status, reason)) {
+ upgrade.error = Error::WebSocketHandshake;
+ return false;
+ }
- // Check for "HTTP/1.1 101"
- auto line = std::string(line_reader.ptr(), line_reader.size());
- if (line.find("HTTP/1.1 101") == std::string::npos) { return false; }
+ // Read the headers even for a rejection so the caller can see why the
+ // server refused the upgrade. A non-101 response may carry a body; it is
+ // deliberately left unread since the caller closes the socket right away.
+ if (!read_headers(strm, upgrade.headers)) {
+ upgrade.error = Error::Read;
+ return false;
+ }
- // Parse headers using existing read_headers
- Headers headers;
- if (!read_headers(strm, headers)) { return false; }
+ const auto &headers = upgrade.headers;
+
+ if (upgrade.status != StatusCode::SwitchingProtocol_101) {
+ upgrade.error = Error::WebSocketHandshake;
+ return false;
+ }
// Verify Upgrade: websocket (case-insensitive)
auto upgrade_it = headers.find("Upgrade");
- if (upgrade_it == headers.end()) { return false; }
- auto upgrade_val = case_ignore::to_lower(upgrade_it->second);
- if (upgrade_val != "websocket") { return false; }
+ if (upgrade_it == headers.end() ||
+ case_ignore::to_lower(upgrade_it->second) != "websocket") {
+ upgrade.error = Error::WebSocketHandshake;
+ return false;
+ }
// Verify Connection header contains "Upgrade" (case-insensitive)
auto connection_it = headers.find("Connection");
- if (connection_it == headers.end()) { return false; }
- auto connection_val = case_ignore::to_lower(connection_it->second);
- if (connection_val.find("upgrade") == std::string::npos) { return false; }
+ if (connection_it == headers.end() ||
+ case_ignore::to_lower(connection_it->second).find("upgrade") ==
+ std::string::npos) {
+ upgrade.error = Error::WebSocketHandshake;
+ return false;
+ }
// Verify Sec-WebSocket-Accept header value
auto it = headers.find("Sec-WebSocket-Accept");
- if (it == headers.end() || it->second != expected_accept) { return false; }
+ if (it == headers.end() || it->second != expected_accept) {
+ upgrade.error = Error::WebSocketHandshake;
+ return false;
+ }
// Extract negotiated subprotocol
auto proto_it = headers.find("Sec-WebSocket-Protocol");
- if (proto_it != headers.end()) { selected_subprotocol = proto_it->second; }
+ if (proto_it != headers.end()) {
+ upgrade.selected_subprotocol = proto_it->second;
+ }
return true;
}
} // namespace fields
bool perform_websocket_handshake(Stream &strm, Request &req,
- std::string &selected_subprotocol) {
+ WebSocketUpgradeResponse &upgrade) {
// Generate random Sec-WebSocket-Key
thread_local std::mt19937 rng(std::random_device{}());
std::string key_bytes(16, '\0');
// and would emit one small write per header.
BufferStream bstrm;
- if (write_request_line(bstrm, req.method, req.path) < 0) { return false; }
+ if (write_request_line(bstrm, req.method, req.path) < 0) {
+ upgrade.error = Error::Write;
+ return false;
+ }
auto error = Error::Success;
if (!check_and_write_headers(bstrm, req.headers, write_headers, error)) {
+ upgrade.error = error;
return false;
}
const auto &data = bstrm.get_buffer();
- if (!write_data(strm, data.data(), data.size())) { return false; }
+ if (!write_data(strm, data.data(), data.size())) {
+ upgrade.error = Error::Write;
+ return false;
+ }
// Verify 101 response and Sec-WebSocket-Accept header
auto expected_accept = websocket_accept_key(client_key);
- return read_websocket_upgrade_response(strm, expected_accept,
- selected_subprotocol);
+ return read_websocket_upgrade_response(strm, expected_accept, upgrade);
}
bool is_ip_address(const std::string &host) {
return ret;
}
-bool setup_client_tls_session(const std::string &host, tls::ctx_t ctx,
- tls::session_t &session, socket_t sock,
- bool server_certificate_verification,
- time_t timeout_sec, time_t timeout_usec) {
+// The parts of session setup that only SSLClient needs, plus the handful
+// WebSocketClient also exposes; everything else takes the defaults, which is
+// what keeps the two clients on one implementation.
+struct ClientTlsSessionOptions {
+ // Both SSLClient and WebSocketClient expose this independently of
+ // certificate verification.
+ bool server_hostname_verification = true;
+ std::function<SSLVerifierResponse(tls::session_t)> session_verifier;
+ // When non-null, guards session creation against concurrent use of the
+ // context. A WebSocketClient is not safe to use from several threads to
+ // begin with, so it passes nothing.
+ std::mutex *ctx_mutex = nullptr;
+#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+ // The caller decides whether Schannel has anything to say about this
+ // connection; see SSLClient::initialize_ssl().
+ bool windows_cert_verification = false;
+#endif
+};
+
+// Filled in on failure for callers that report error details.
+struct ClientTlsSessionError {
+ Error error = Error::Success;
+ int ssl_error = 0;
+ uint64_t backend_error = 0;
+};
+
+// Establishes a client TLS session on an already connected socket. On failure
+// the session is left for the caller to free: SSLClient frees it right away,
+// WebSocketClient keeps it in a member that shutdown_and_close() cleans up.
+bool setup_client_tls_session(
+ const std::string &host, tls::ctx_t ctx, tls::session_t &session,
+ socket_t sock, bool server_certificate_verification, time_t timeout_sec,
+ time_t timeout_usec, ClientTlsSessionError *out_error = nullptr,
+ const ClientTlsSessionOptions &options = ClientTlsSessionOptions()) {
using namespace tls;
- if (!ctx) { return false; }
+ auto fail = [&](Error error, int ssl_error, uint64_t backend_error) {
+ if (out_error) {
+ out_error->error = error;
+ out_error->ssl_error = ssl_error;
+ out_error->backend_error = backend_error;
+ }
+ return false;
+ };
- bool is_ip = is_ip_address(host);
+ if (!ctx) {
+ session = nullptr;
+ return fail(Error::SSLConnection, 0, 0);
+ }
#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
- // Chain verification happens during the handshake even for IP hosts; the
- // certificate identity is verified post-handshake via verify_hostname()
+ // Mbed TLS and wolfSSL need the verification mode set explicitly; OpenSSL
+ // uses SSL_VERIFY_NONE and does all verification post-handshake. Chain
+ // verification happens during the handshake even for IP hosts; the
+ // certificate identity is verified post-handshake via verify_hostname().
set_verify_client(ctx, server_certificate_verification);
#endif
- session = create_session(ctx, sock);
- if (!session) { return false; }
+ {
+ std::unique_lock<std::mutex> guard;
+ if (options.ctx_mutex) {
+ guard = std::unique_lock<std::mutex>(*options.ctx_mutex);
+ }
+ session = create_session(ctx, sock);
+ }
+ if (!session) { return fail(Error::SSLConnection, 0, get_error()); }
- // RFC 6066: SNI must not be set for IP addresses. On Mbed TLS and wolfSSL
- // set_hostname also sets SNI, so it must be skipped for IP hosts as well;
- // their identity is checked post-handshake below instead.
- if (!is_ip) {
- if (server_certificate_verification) {
- set_hostname(session, host.c_str());
- } else {
- set_sni(session, host.c_str());
+ // 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.
+ if (!is_ip_address(host)) {
+ if (!set_sni(session, host.c_str(), options.server_hostname_verification)) {
+ return fail(Error::SSLConnection, 0, get_error());
}
}
- if (!connect_nonblocking(session, sock, timeout_sec, timeout_usec, nullptr)) {
- return false;
+ 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);
}
- if (server_certificate_verification) {
- if (get_verify_result(session) != 0) { return false; }
+ auto verification_status = SSLVerifierResponse::NoDecisionMade;
+ if (options.session_verifier) {
+ verification_status = options.session_verifier(session);
+ }
+
+ if (verification_status == SSLVerifierResponse::CertificateRejected) {
+ return fail(Error::SSLServerVerification, 0, get_error());
+ }
+
+ if (verification_status == SSLVerifierResponse::NoDecisionMade &&
+ server_certificate_verification) {
+ auto verify_result = get_verify_result(session);
+ if (verify_result != 0) {
+ return fail(Error::SSLServerVerification, 0,
+ static_cast<uint64_t>(verify_result));
+ }
- // Identity check against the peer certificate, post-handshake for all
- // backends (same as SSLClient). For IP hosts this is the only identity
- // verification since no hostname is bound during the handshake.
auto server_cert = get_peer_cert(session);
- if (!server_cert) { return false; }
+ if (!server_cert) {
+ return fail(Error::SSLServerVerification, 0, get_error());
+ }
auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); });
- if (!verify_hostname(server_cert, host.c_str())) { return false; }
+
+ // Identity check against the peer certificate, post-handshake for all
+ // backends. For IP hosts this is the only identity verification, since no
+ // 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());
+ }
+ }
+
+#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+ // Additional Windows Schannel verification.
+ // This provides real-time certificate validation with Windows Update
+ // integration, working with both OpenSSL and MbedTLS backends.
+ if (options.windows_cert_verification) {
+ std::vector<unsigned char> der;
+ if (get_cert_der(server_cert, der)) {
+ uint64_t wincrypt_error = 0;
+ if (!verify_cert_with_windows_schannel(
+ der, host, options.server_hostname_verification,
+ wincrypt_error)) {
+ return fail(Error::SSLServerVerification, 0, wincrypt_error);
+ }
+ }
+ }
+#endif
}
return true;
case Error::HTTPParsing: return "HTTP parsing failed";
case Error::InvalidRangeHeader: return "Invalid Range header";
case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding";
+ case Error::WebSocketHandshake: return "WebSocket handshake failed";
default: break;
}
return prepare_host_string(host) + ":" + std::to_string(port);
}
+// Value for the Host header a client sends when the caller supplied none.
+// Only the value: callers decide where in their header list it goes.
+std::string make_default_host_header_value(const std::string &host,
+ int port, bool is_ssl,
+ int address_family) {
+ if (address_family == AF_UNIX) { return "localhost"; }
+ return make_host_and_port_string(host, port, is_ssl);
+}
+
+void add_default_user_agent_header(Request &req) {
+#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT
+ if (!req.has_header("User-Agent")) {
+ req.set_header("User-Agent",
+ std::string("cpp-httplib/") + CPPHTTPLIB_VERSION);
+ }
+#else
+ (void)req;
+#endif
+}
+
bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out);
NormalizedTarget normalize_target(const std::string &host);
bool ip_in_cidr(const IPBytes &ip, const IPBytes &net, int prefix_bits);
bool Server::handle_file_request(Request &req, Response &res) {
for (const auto &entry : base_dirs_) {
- // Prefix match
- if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) {
+ // Prefix match, on a path segment boundary. A mount point of "/mount"
+ // covers "/mount" and "/mount/...", but must not swallow "/mountdir/...".
+ // One that already ends in '/' (the root mount among them) carries its own
+ // boundary; set_mount_point() guarantees the mount point is not empty.
+ if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point) &&
+ (entry.mount_point.back() == '/' ||
+ 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;
nullptr, &websocket_upgraded);
});
- detail::shutdown_socket(sock);
- detail::close_socket(sock);
+ detail::drain_and_close_socket(sock);
return ret;
}
if (!line_reader.getline()) { return false; }
-#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
- thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n");
-#else
- thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n");
-#endif
-
- std::cmatch m;
- if (!std::regex_match(line_reader.ptr(), m, re)) {
+ if (!detail::parse_status_line(line_reader.ptr(), res.version, res.status,
+ res.reason)) {
return req.method == "CONNECT";
}
- res.version = std::string(m[1]);
- res.status = std::stoi(std::string(m[2]));
- res.reason = std::string(m[3]);
// Ignore '100 Continue' (only when not using Expect: 100-continue explicitly)
while (skip_100_continue && res.status == StatusCode::Continue_100) {
if (!line_reader.getline()) { return false; } // CRLF
if (!line_reader.getline()) { return false; } // next response line
- if (!std::regex_match(line_reader.ptr(), m, re)) { return false; }
- res.version = std::string(m[1]);
- res.status = std::stoi(std::string(m[2]));
- res.reason = std::string(m[3]);
+ if (!detail::parse_status_line(line_reader.ptr(), res.version, res.status,
+ res.reason)) {
+ return false;
+ }
}
return true;
// RFC 9110 5.3 recommends sending control data such as Host first, so
// prepend it rather than appending it after the caller's own fields.
if (!r.has_header("Host")) {
- if (address_family_ == AF_UNIX) {
- r.headers.emplace_front("Host", "localhost");
- } else {
- r.headers.emplace_front(
- "Host", detail::make_host_and_port_string(host_, port_, is_ssl()));
- }
+ r.headers.emplace_front(
+ "Host", detail::make_default_host_header_value(host_, port_, is_ssl(),
+ address_family_));
}
if (!r.has_header("Accept")) { r.headers.emplace("Accept", "*/*"); }
r.set_header("Accept-Encoding", accept_encoding);
}
-#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT
- if (!r.has_header("User-Agent")) {
- auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION;
- r.set_header("User-Agent", agent);
- }
-#endif
+ detail::add_default_user_agent_header(r);
}
if (!r.body.empty()) {
bool SSLClient::load_certs() {
auto ret = true;
+ // call_once rather than the plain flag WebSocketClient::create_stream() uses:
+ // one client is shared across concurrent requests here.
std::call_once(initialize_cert_, [&]() {
std::lock_guard<std::mutex> guard(ctx_mutex_);
}
bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
- using namespace tls;
-
// Load CA certificates if server verification is enabled
if (server_certificate_verification_) {
if (!load_certs()) {
}
}
- bool is_ip = detail::is_ip_address(host_);
-
-#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
- // MbedTLS/wolfSSL need explicit verification mode (OpenSSL uses
- // SSL_VERIFY_NONE by default and performs all verification post-handshake).
- // Chain verification happens during the handshake even for IP hosts; the
- // certificate identity is verified post-handshake via verify_hostname().
- set_verify_client(ctx_, server_certificate_verification_);
+ detail::ClientTlsSessionOptions options;
+ options.server_hostname_verification = server_hostname_verification_;
+ options.session_verifier = session_verifier_;
+ options.ctx_mutex = &ctx_mutex_;
+#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+ // Skip Schannel when a custom CA cert is specified, as the Windows
+ // certificate store would not know about user-provided CA certificates.
+ // Also skip when system CA trust is explicitly disabled.
+ 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_;
#endif
- // Create TLS session
- session_t session = nullptr;
- {
- std::lock_guard<std::mutex> guard(ctx_mutex_);
- session = create_session(ctx_, socket.sock);
- }
-
- if (!session) {
- error = Error::SSLConnection;
- last_backend_error_ = get_error();
- return false;
- }
+ tls::session_t session = nullptr;
// Use scope_exit to ensure session is freed on error paths
bool success = false;
auto session_guard = detail::scope_exit([&] {
- if (!success) { free_session(session); }
+ if (!success) { tls::free_session(session); }
});
- // Set SNI extension (skip for IP addresses per RFC 6066).
- // On MbedTLS, set_sni also enables hostname verification internally.
- // On OpenSSL, set_sni only sets SNI; verification is done post-handshake.
- if (!is_ip) {
- if (!set_sni(session, host_.c_str())) {
- error = Error::SSLConnection;
- last_backend_error_ = get_error();
- return false;
- }
- }
-
- // Perform non-blocking TLS handshake with timeout
- TlsError tls_err;
- if (!connect_nonblocking(session, socket.sock, connection_timeout_sec_,
- connection_timeout_usec_, &tls_err)) {
- last_ssl_error_ = static_cast<int>(tls_err.code);
- last_backend_error_ = tls_err.backend_code;
- if (tls_err.code == ErrorCode::CertVerifyFailed) {
- error = Error::SSLServerVerification;
- } else if (tls_err.code == ErrorCode::HostnameMismatch) {
- error = Error::SSLServerHostnameVerification;
- } else {
- error = Error::SSLConnection;
- }
- output_error_log(error, nullptr);
- return false;
- }
-
- // Post-handshake session verifier callback
- auto verification_status = SSLVerifierResponse::NoDecisionMade;
- if (session_verifier_) { verification_status = session_verifier_(session); }
-
- if (verification_status == SSLVerifierResponse::CertificateRejected) {
- last_backend_error_ = get_error();
- error = Error::SSLServerVerification;
+ detail::ClientTlsSessionError tls_error;
+ if (!detail::setup_client_tls_session(
+ host_, ctx_, session, socket.sock, server_certificate_verification_,
+ connection_timeout_sec_, connection_timeout_usec_, &tls_error,
+ options)) {
+ error = tls_error.error;
+ last_ssl_error_ = tls_error.ssl_error;
+ last_backend_error_ = tls_error.backend_error;
output_error_log(error, nullptr);
return false;
}
- // Default server certificate verification
- if (verification_status == SSLVerifierResponse::NoDecisionMade &&
- server_certificate_verification_) {
- verify_result_ = tls::get_verify_result(session);
- if (verify_result_ != 0) {
- last_backend_error_ = static_cast<uint64_t>(verify_result_);
- error = Error::SSLServerVerification;
- output_error_log(error, nullptr);
- return false;
- }
-
- auto server_cert = get_peer_cert(session);
- if (!server_cert) {
- last_backend_error_ = get_error();
- error = Error::SSLServerVerification;
- output_error_log(error, nullptr);
- return false;
- }
- auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); });
-
- // Hostname verification (post-handshake for all cases).
- // On OpenSSL, verification is always post-handshake (SSL_VERIFY_NONE).
- // On MbedTLS, set_sni already enabled hostname verification during
- // handshake for non-IP hosts, but this check is still needed for IP
- // addresses where SNI is not set.
- if (server_hostname_verification_) {
- if (!verify_hostname(server_cert, host_.c_str())) {
- last_backend_error_ = hostname_mismatch_code();
- error = Error::SSLServerHostnameVerification;
- output_error_log(error, nullptr);
- return false;
- }
- }
-
-#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
- // Additional Windows Schannel verification.
- // This provides real-time certificate validation with Windows Update
- // integration, working with both OpenSSL and MbedTLS backends.
- // Skip when a custom CA cert is specified, as the Windows certificate
- // store would not know about user-provided CA certificates. Also skip
- // when system CA trust is explicitly disabled.
- if (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_) {
- std::vector<unsigned char> der;
- if (get_cert_der(server_cert, der)) {
- uint64_t wincrypt_error = 0;
- if (!detail::verify_cert_with_windows_schannel(
- der, host_, server_hostname_verification_, wincrypt_error)) {
- last_backend_error_ = wincrypt_error;
- error = Error::SSLServerVerification;
- output_error_log(error, nullptr);
- return false;
- }
- }
- }
-#endif
- }
-
success = true;
socket.ssl = session;
return true;
if (session) { SSL_free(static_cast<SSL *>(session)); }
}
-bool set_sni(session_t session, const char *hostname) {
+bool set_sni(session_t session, const char *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
+ // 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.
#if defined(OPENSSL_IS_BORINGSSL)
return SSL_set_tlsext_host_name(ssl, hostname) == 1;
#else
#endif
}
-bool set_hostname(session_t session, const char *hostname) {
- if (!session || !hostname) return false;
-
- auto ssl = static_cast<SSL *>(session);
-
- // Enable hostname verification
- auto param = SSL_get0_param(ssl);
- if (!param) return false;
-
- if (detail::is_ip_address(hostname)) {
- // RFC 6066: SNI must not be set for IP addresses; verify against the
- // certificate's IP SANs instead of its DNS names
- if (X509_VERIFY_PARAM_set1_ip_asc(param, hostname) != 1) { return false; }
- } else {
- // Set SNI (Server Name Indication)
- if (!set_sni(session, hostname)) { return false; }
-
- X509_VERIFY_PARAM_set_hostflags(param,
- X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
- if (X509_VERIFY_PARAM_set1_host(param, hostname, 0) != 1) { return false; }
- }
-
- SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr);
- return true;
-}
-
TlsError connect(session_t session) {
if (!session) { return TlsError(); }
unsigned char peeked_byte = 0;
bool has_peeked_byte = false;
+ // Set by set_sni() when the caller disabled hostname verification, so the
+ // verify callback can clear the CN/SAN mismatch flag while still enforcing
+ // the rest of the chain (Mbed TLS ties SNI and identity checking together;
+ // OpenSSL and wolfSSL keep them independent).
+ bool suppress_hostname_mismatch = false;
+
+ // Copied from the owning MbedTlsContext at creation. set_sni() uses this to
+ // decide which verify callback to install when hostname verification is
+ // disabled: mbedtls_verify_callback() when a user callback is genuinely
+ // wired for this context, or a self-contained one otherwise, so a session
+ // that never opted into a callback never consults the process-wide
+ // set_verify_callback() slot (which some other, unrelated client may have
+ // populated).
+ bool has_verify_callback = false;
+
MbedTlsSession() { mbedtls_ssl_init(&ssl); }
~MbedTlsSession() { mbedtls_ssl_free(&ssl); }
}
// Helper to map Mbed TLS error to ErrorCode
-ErrorCode map_mbedtls_error(int ret, int &out_errno) {
+ErrorCode map_mbedtls_error(int ret, int &out_errno,
+ uint32_t verify_flags) {
if (ret == 0) { return ErrorCode::Success; }
if (ret == MBEDTLS_ERR_SSL_WANT_READ) { return ErrorCode::WantRead; }
if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return ErrorCode::WantWrite; }
return ErrorCode::SyscallError;
}
if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {
+ // Unlike OpenSSL/wolfSSL, Mbed TLS folds the CN/SAN identity check into
+ // the handshake's chain verification (see set_sni()); a mismatch there
+ // is reported the same way as any other verify_flags bit. Report it as
+ // HostnameMismatch, matching the other backends and the post-handshake
+ // identity check below, but only when naming is the sole problem -
+ // if the chain itself is also untrusted/expired/etc., that takes
+ // priority over the naming detail.
+ if (verify_flags == static_cast<uint32_t>(hostname_mismatch_code())) {
+ return ErrorCode::HostnameMismatch;
+ }
return ErrorCode::CertVerifyFailed;
}
return ErrorCode::Fatal;
}
+// Populates a TlsError from a failed (non-zero) mbedtls_ssl_handshake()
+// return value, including the verify-flags-dependent HostnameMismatch
+// mapping; shared by connect() and connect_nonblocking() so the
+// backend_code policy for that mapping only lives in one place.
+void fill_mbedtls_tls_error(TlsError &err, mbedtls_ssl_context &ssl,
+ int ret) {
+ auto verify_flags = mbedtls_ssl_get_verify_result(&ssl);
+ err.code = map_mbedtls_error(ret, err.sys_errno, verify_flags);
+ err.backend_code = err.code == ErrorCode::HostnameMismatch
+ ? static_cast<uint64_t>(verify_flags)
+ : static_cast<uint64_t>(-ret);
+}
+
// A TLS 1.3 NewSessionTicket (signaled by default on Mbed TLS 4.x) is a
// non-fatal notification delivered between records, not an error and not
// application data, so I/O calls that see it should just be retried. Kept in
return 0; // Accept any SNI
}
+void mbedtls_clear_cn_mismatch(uint32_t *flags) {
+ *flags &= ~static_cast<uint32_t>(hostname_mismatch_code());
+}
+
+// Verify callback used when hostname verification is disabled for a session
+// that has no user-supplied verify callback of its own (MbedTlsSession::
+// has_verify_callback is false). Deliberately does not consult
+// get_verify_callback(): that slot is process-wide, so reading it here would
+// pick up whatever another, unrelated client last installed there.
+int mbedtls_mask_hostname_mismatch_callback(void *data,
+ mbedtls_x509_crt *, int,
+ uint32_t *flags) {
+ (void)data;
+ mbedtls_clear_cn_mismatch(flags);
+ return 0;
+}
+
int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt,
int cert_depth, uint32_t *flags);
// MbedTLS verify callback wrapper
int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt,
int cert_depth, uint32_t *flags) {
- auto &callback = get_verify_callback();
- if (!callback) { return 0; } // Continue with default verification
-
// data points to the MbedTlsSession
auto *session = static_cast<MbedTlsSession *>(data);
+ // set_sni() disabled hostname verification for this session: drop the
+ // CN/SAN mismatch flag so it doesn't fail the chain check below, mirroring
+ // the OpenSSL/wolfSSL backends where identity checking is independent of
+ // SNI. The final pass/fail decision still comes from the remaining flags
+ // (or, below, from the user's own verify callback).
+ if (session && session->suppress_hostname_mismatch) {
+ mbedtls_clear_cn_mismatch(flags);
+ }
+
+ auto &callback = get_verify_callback();
+ if (!callback) { return 0; } // Continue with default verification
+
// Build context
VerifyContext verify_ctx;
verify_ctx.session = static_cast<session_t>(session);
// Set per-session verify callback with session pointer if callback is
// registered
+ session->has_verify_callback = mctx->has_verify_callback;
if (mctx->has_verify_callback) {
mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback,
session);
if (session) { delete static_cast<impl::MbedTlsSession *>(session); }
}
-bool set_sni(session_t session, const char *hostname) {
+bool set_sni(session_t session, const char *hostname,
+ bool verify_hostname) {
if (!session || !hostname) { return false; }
auto msession = static_cast<impl::MbedTlsSession *>(session);
+ // mbedtls_ssl_set_hostname() both sends the SNI extension and binds the
+ // handshake-time CN/SAN check to `hostname`; the two can't be requested
+ // independently, so a disabled hostname check is handled below by masking
+ // the resulting mismatch flag instead of skipping this call.
int ret = mbedtls_ssl_set_hostname(&msession->ssl, hostname);
if (ret != 0) {
impl::mbedtls_last_error() = ret;
}
msession->hostname = hostname;
- return true;
-}
-bool set_hostname(session_t session, const char *hostname) {
- // In Mbed TLS, set_hostname also sets up hostname verification
- return set_sni(session, hostname);
+ if (!verify_hostname) {
+ msession->suppress_hostname_mismatch = true;
+ // If a user verify callback is already wired for this session,
+ // mbedtls_verify_callback() masks the mismatch flag itself before
+ // consulting it (see suppress_hostname_mismatch above) - reinstalling it
+ // here would be redundant. Otherwise install the self-contained masking
+ // callback, which never touches the process-wide callback slot.
+ if (!msession->has_verify_callback) {
+ mbedtls_ssl_set_verify(&msession->ssl,
+ impl::mbedtls_mask_hostname_mismatch_callback,
+ msession);
+ }
+ }
+
+ return true;
}
TlsError connect(session_t session) {
if (ret == 0) {
err.code = ErrorCode::Success;
} else {
- err.code = impl::map_mbedtls_error(ret, err.sys_errno);
- err.backend_code = static_cast<uint64_t>(-ret);
+ impl::fill_mbedtls_tls_error(err, msession->ssl, ret);
impl::mbedtls_last_error() = ret;
}
}
// TlsError or timeout
- if (err) {
- err->code = impl::map_mbedtls_error(ret, err->sys_errno);
- err->backend_code = static_cast<uint64_t>(-ret);
- }
+ if (err) { impl::fill_mbedtls_tls_error(*err, msession->ssl, ret); }
impl::mbedtls_last_error() = ret;
return false;
}
return 0;
}
- err.code = impl::map_mbedtls_error(ret, err.sys_errno);
+ err.code = impl::map_mbedtls_error(ret, err.sys_errno, 0);
err.backend_code = static_cast<uint64_t>(-ret);
impl::mbedtls_last_error() = ret;
// mbedTLS signals a clean close_notify via a negative error code rather
return 0;
}
- err.code = impl::map_mbedtls_error(ret, err.sys_errno);
+ err.code = impl::map_mbedtls_error(ret, err.sys_errno, 0);
err.backend_code = static_cast<uint64_t>(-ret);
impl::mbedtls_last_error() = ret;
return -1;
if (session) { delete static_cast<impl::WolfSSLSession *>(session); }
}
-bool set_sni(session_t session, const char *hostname) {
+bool set_sni(session_t session, const char *hostname,
+ bool verify_hostname) {
if (!session || !hostname) { return false; }
auto wsession = static_cast<impl::WolfSSLSession *>(session);
return false;
}
- // Also set hostname for verification
- wolfSSL_check_domain_name(wsession->ssl, hostname);
+ // wolfSSL_check_domain_name binds identity checking to the handshake,
+ // separately from the SNI extension sent above; skip it when hostname
+ // verification is disabled so only the chain is checked, matching OpenSSL.
+ if (verify_hostname) { wolfSSL_check_domain_name(wsession->ssl, hostname); }
wsession->hostname = hostname;
return true;
}
-bool set_hostname(session_t session, const char *hostname) {
- // In wolfSSL, set_hostname also sets up hostname verification
- return set_sni(session, hostname);
-}
-
TlsError connect(session_t session) {
TlsError err;
if (!session) {
}
}
+#ifdef CPPHTTPLIB_SSL_ENABLED
+WebSocketClient::WebSocketClient(
+ const std::string &scheme_host_port_path, const PemMemory &pem,
+ const Headers &headers)
+ : WebSocketClient(scheme_host_port_path, headers) {
+ // For ws:// URLs the client certificate is silently ignored, consistent
+ // with the TLS-only setters such as set_ca_cert_path().
+ if (is_valid_ && is_ssl_ && pem.cert_pem && pem.key_pem) {
+ if (!tls::set_client_cert_pem(tls_ctx_, pem.cert_pem, pem.key_pem,
+ pem.private_key_password)) {
+ tls::free_context(tls_ctx_);
+ tls_ctx_ = nullptr;
+ is_valid_ = false;
+ }
+ }
+}
+#endif
+
WebSocketClient::~WebSocketClient() {
shutdown_and_close();
#ifdef CPPHTTPLIB_SSL_ENABLED
}
}
-bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm) {
+bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
+ Error &error, int &ssl_error,
+ uint64_t &ssl_backend_error) {
#ifdef CPPHTTPLIB_SSL_ENABLED
if (is_ssl_) {
+ // A plain flag rather than SSLClient::load_certs()'s call_once: connect()
+ // is not safe to call concurrently on one client to begin with, since
+ // nothing else here is guarded either.
if (server_certificate_verification_ && !certs_loaded_) {
uint64_t backend_error = 0;
- detail::load_client_ca_config(tls_ctx_, ca_cert_file_path_, std::string(),
- custom_ca_loaded_, system_ca_mode_,
- backend_error);
+ detail::load_client_ca_config(tls_ctx_, ca_cert_file_path_,
+ ca_cert_dir_path_, custom_ca_loaded_,
+ system_ca_mode_, backend_error);
certs_loaded_ = true;
}
+ detail::ClientTlsSessionOptions options;
+ options.server_hostname_verification = server_hostname_verification_;
+
+ detail::ClientTlsSessionError tls_error;
if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_,
server_certificate_verification_,
- read_timeout_sec_,
- read_timeout_usec_)) {
+ read_timeout_sec_, read_timeout_usec_,
+ &tls_error, options)) {
+ error = tls_error.error;
+ ssl_error = tls_error.ssl_error;
+ ssl_backend_error = tls_error.backend_error;
return false;
}
write_timeout_sec_, write_timeout_usec_));
return true;
}
+#else
+ (void)error;
+ (void)ssl_error;
+ (void)ssl_backend_error;
#endif
strm = std::unique_ptr<Stream>(
new detail::SocketStream(sock_, read_timeout_sec_, read_timeout_usec_,
#endif
if (!req.has_header("Host")) {
- if (address_family_ == AF_UNIX) {
- req.headers.emplace("Host", "localhost");
- } else {
- req.headers.emplace(
- "Host", detail::make_host_and_port_string(host_, port_, is_ssl));
- }
+ req.headers.emplace("Host", detail::make_default_host_header_value(
+ host_, port_, is_ssl, address_family_));
}
-#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT
- if (!req.has_header("User-Agent")) {
- auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION;
- req.set_header("User-Agent", agent);
- }
-#endif
+ detail::add_default_user_agent_header(req);
}
-bool WebSocketClient::connect() {
- if (!is_valid_) { return false; }
+Result WebSocketClient::connect() {
+ if (!is_valid_) { return Result{Error::Connection, -1, Headers{}}; }
shutdown_and_close();
// Check is custom IP or hostname specified for host_
std::string ip;
detail::apply_addr_map(addr_map_, host_, connect_host, ip);
- Error error;
+ auto error = Error::Success;
sock_ = detail::create_client_socket(
connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
write_timeout_usec_, interface_, error);
- if (sock_ == INVALID_SOCKET) { return false; }
+ if (sock_ == INVALID_SOCKET) {
+ if (error == Error::Success) { error = Error::Connection; }
+ return Result{error, -1, Headers{}};
+ }
std::unique_ptr<Stream> strm;
- if (!create_stream(strm)) {
+ auto stream_error = Error::SSLConnection;
+ int ssl_error = 0;
+ uint64_t ssl_backend_error = 0;
+ if (!create_stream(strm, stream_error, ssl_error, ssl_backend_error)) {
shutdown_and_close();
- return false;
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ return Result{stream_error, -1, Headers{}, ssl_error, ssl_backend_error};
+#else
+ return Result{stream_error, -1, Headers{}};
+#endif
}
Request req;
req.headers = headers_;
prepare_default_headers(req);
- std::string selected_subprotocol;
- if (!detail::perform_websocket_handshake(*strm, req, selected_subprotocol)) {
+ detail::WebSocketUpgradeResponse upgrade;
+ if (!detail::perform_websocket_handshake(*strm, req, upgrade)) {
shutdown_and_close();
- return false;
+ return Result{upgrade.error, upgrade.status, std::move(upgrade.headers)};
}
- subprotocol_ = std::move(selected_subprotocol);
+ subprotocol_ = std::move(upgrade.selected_subprotocol);
ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false,
websocket_ping_interval_sec_,
websocket_max_missed_pongs_));
- return true;
+ return Result{Error::Success, upgrade.status, std::move(upgrade.headers)};
}
ReadResult WebSocketClient::read(std::string &msg) {
#ifdef CPPHTTPLIB_SSL_ENABLED
-void WebSocketClient::set_ca_cert_path(const std::string &path) {
- ca_cert_file_path_ = path;
+void
+WebSocketClient::set_ca_cert_path(const std::string &ca_cert_file_path,
+ const std::string &ca_cert_dir_path) {
+ ca_cert_file_path_ = ca_cert_file_path;
+ ca_cert_dir_path_ = ca_cert_dir_path;
}
void WebSocketClient::set_ca_cert_store(tls::ca_store_t store) {
server_certificate_verification_ = enabled;
}
+void WebSocketClient::enable_server_hostname_verification(bool enabled) {
+ server_hostname_verification_ = enabled;
+}
+
void WebSocketClient::enable_system_ca(bool enabled) {
system_ca_mode_ = enabled ? SystemCAMode::Enabled : SystemCAMode::Disabled;
}