#include "httplib.h" namespace httplib { /* * Implementation that will be part of the .cc file if split into .h + .cc. */ namespace stream { // stream::Result implementations Result::Result() : chunk_size_(8192) {} Result::Result(ClientImpl::StreamHandle &&handle, size_t chunk_size) : handle_(std::move(handle)), chunk_size_(chunk_size) {} Result::Result(Result &&other) noexcept : handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)), current_size_(other.current_size_), chunk_size_(other.chunk_size_), finished_(other.finished_) { other.current_size_ = 0; other.finished_ = true; } Result &Result::operator=(Result &&other) noexcept { if (this != &other) { handle_ = std::move(other.handle_); buffer_ = std::move(other.buffer_); current_size_ = other.current_size_; chunk_size_ = other.chunk_size_; finished_ = other.finished_; other.current_size_ = 0; other.finished_ = true; } return *this; } bool Result::is_valid() const { return handle_.is_valid(); } Result::operator bool() const { return is_valid(); } int Result::status() const { return handle_.response ? handle_.response->status : -1; } const Headers &Result::headers() const { static const Headers empty_headers; return handle_.response ? handle_.response->headers : empty_headers; } std::string Result::get_header_value(const std::string &key, const char *def) const { return handle_.response ? handle_.response->get_header_value(key, def) : def; } bool Result::has_header(const std::string &key) const { return handle_.response ? handle_.response->has_header(key) : false; } Error Result::error() const { return handle_.error; } Error Result::read_error() const { return handle_.get_read_error(); } bool Result::has_read_error() const { return handle_.has_read_error(); } bool Result::next() { if (!handle_.is_valid() || finished_) { return false; } if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); } ssize_t n = handle_.read(&buffer_[0], chunk_size_); if (n > 0) { current_size_ = static_cast(n); return true; } current_size_ = 0; finished_ = true; return false; } const char *Result::data() const { return buffer_.data(); } size_t Result::size() const { return current_size_; } std::string Result::read_all() { std::string result; while (next()) { result.append(data(), size()); } return result; } } // namespace stream namespace sse { // SSEMessage implementations SSEMessage::SSEMessage() : event("message") {} void SSEMessage::clear() { event = "message"; data.clear(); id.clear(); } // SSEClient implementations SSEClient::SSEClient(Client &client, const std::string &path) : client_(client), path_(path) {} SSEClient::SSEClient(Client &client, const std::string &path, const Headers &headers) : client_(client), path_(path), headers_(headers) {} SSEClient::~SSEClient() { stop(); } SSEClient &SSEClient::on_message(MessageHandler handler) { on_message_ = std::move(handler); return *this; } SSEClient &SSEClient::on_event(const std::string &type, MessageHandler handler) { event_handlers_[type] = std::move(handler); return *this; } SSEClient &SSEClient::on_open(OpenHandler handler) { on_open_ = std::move(handler); return *this; } SSEClient &SSEClient::on_error(ErrorHandler handler) { on_error_ = std::move(handler); return *this; } SSEClient &SSEClient::set_reconnect_interval(int ms) { reconnect_interval_ms_ = ms; return *this; } SSEClient &SSEClient::set_max_reconnect_attempts(int n) { max_reconnect_attempts_ = n; return *this; } SSEClient &SSEClient::set_headers(const Headers &headers) { std::lock_guard lock(headers_mutex_); headers_ = headers; return *this; } bool SSEClient::is_connected() const { return connected_.load(); } const std::string &SSEClient::last_event_id() const { return last_event_id_; } void SSEClient::start() { running_.store(true); run_event_loop(); } void SSEClient::start_async() { running_.store(true); async_thread_ = std::thread([this]() { run_event_loop(); }); } void SSEClient::stop() { running_.store(false); client_.stop(); // Cancel any pending operations if (async_thread_.joinable()) { async_thread_.join(); } } bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms) { // Blank line signals end of event if (line.empty() || line == "\r") { return true; } // Lines starting with ':' are comments (ignored) if (!line.empty() && line[0] == ':') { return false; } // Find the colon separator auto colon_pos = line.find(':'); if (colon_pos == std::string::npos) { // Line with no colon is treated as field name with empty value return false; } auto field = line.substr(0, colon_pos); std::string value; // Value starts after colon, skip optional single space if (colon_pos + 1 < line.size()) { auto value_start = colon_pos + 1; if (line[value_start] == ' ') { value_start++; } value = line.substr(value_start); // Remove trailing \r if present if (!value.empty() && value.back() == '\r') { value.pop_back(); } } // Handle known fields if (field == "event") { msg.event = value; } else if (field == "data") { // Multiple data lines are concatenated with newlines if (!msg.data.empty()) { msg.data += "\n"; } msg.data += value; } else if (field == "id") { // Empty id is valid (clears the last event ID) msg.id = value; } else if (field == "retry") { // Parse retry interval in milliseconds { int v = 0; auto res = detail::from_chars(value.data(), value.data() + value.size(), v); if (res.ec == std::errc{}) { retry_ms = v; } } } // Unknown fields are ignored per SSE spec return false; } void SSEClient::run_event_loop() { auto reconnect_count = 0; while (running_.load()) { // Build headers, including Last-Event-ID if we have one Headers request_headers; { std::lock_guard lock(headers_mutex_); request_headers = headers_; } if (!last_event_id_.empty()) { request_headers.emplace("Last-Event-ID", last_event_id_); } // Open streaming connection auto result = stream::Get(client_, path_, request_headers); // Connection error handling if (!result) { connected_.store(false); if (on_error_) { on_error_(result.error()); } if (!should_reconnect(reconnect_count)) { break; } wait_for_reconnect(); reconnect_count++; continue; } if (result.status() != StatusCode::OK_200) { connected_.store(false); if (on_error_) { on_error_(Error::Connection); } // For certain errors, don't reconnect. // Note: 401 is intentionally absent so that handlers can refresh // credentials via set_headers() and let the client reconnect. if (result.status() == StatusCode::NoContent_204 || result.status() == StatusCode::NotFound_404 || result.status() == StatusCode::Forbidden_403) { break; } if (!should_reconnect(reconnect_count)) { break; } wait_for_reconnect(); reconnect_count++; continue; } // Connection successful connected_.store(true); reconnect_count = 0; if (on_open_) { on_open_(); } // Event receiving loop std::string buffer; SSEMessage current_msg; while (running_.load() && result.next()) { buffer.append(result.data(), result.size()); // Process complete lines in the buffer size_t line_start = 0; size_t newline_pos; while ((newline_pos = buffer.find('\n', line_start)) != std::string::npos) { auto line = buffer.substr(line_start, newline_pos - line_start); line_start = newline_pos + 1; // Parse the line and check if event is complete auto event_complete = parse_sse_line(line, current_msg, reconnect_interval_ms_); if (event_complete && !current_msg.data.empty()) { // Update last_event_id for reconnection if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; } // Dispatch event to appropriate handler dispatch_event(current_msg); current_msg.clear(); } } // Keep unprocessed data in buffer buffer.erase(0, line_start); } // Connection ended connected_.store(false); if (!running_.load()) { break; } // Check for read errors if (result.has_read_error()) { if (on_error_) { on_error_(result.read_error()); } } if (!should_reconnect(reconnect_count)) { break; } wait_for_reconnect(); reconnect_count++; } connected_.store(false); } void SSEClient::dispatch_event(const SSEMessage &msg) { // Check for specific event type handler first auto it = event_handlers_.find(msg.event); if (it != event_handlers_.end()) { it->second(msg); return; } // Fall back to generic message handler if (on_message_) { on_message_(msg); } } bool SSEClient::should_reconnect(int count) const { if (!running_.load()) { return false; } if (max_reconnect_attempts_ == 0) { return true; } // unlimited return count < max_reconnect_attempts_; } void SSEClient::wait_for_reconnect() { // Use small increments to check running_ flag frequently auto waited = 0; while (running_.load() && waited < reconnect_interval_ms_) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); waited += 100; } } } // namespace sse #ifdef CPPHTTPLIB_SSL_ENABLED /* * TLS abstraction layer - internal function declarations * These are implementation details and not part of the public API. */ namespace tls { // Client context ctx_t create_client_context(); void free_context(ctx_t ctx); bool set_min_version(ctx_t ctx, Version version); bool load_ca_pem(ctx_t ctx, const char *pem, size_t len); bool load_ca_file(ctx_t ctx, const char *file_path); bool load_ca_dir(ctx_t ctx, const char *dir_path); bool load_system_certs(ctx_t ctx); bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password); bool set_client_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password); // Server context ctx_t create_server_context(); bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password); bool set_server_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password); bool set_client_ca_file(ctx_t ctx, const char *ca_file, const char *ca_dir); void set_verify_client(ctx_t ctx, bool require); // 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); // Handshake (non-blocking capable) TlsError connect(session_t session); TlsError accept(session_t session); // Handshake with timeout (blocking until timeout) bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err); bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err); // I/O (non-blocking capable) ssize_t read(session_t session, void *buf, size_t len, TlsError &err); ssize_t write(session_t session, const void *buf, size_t len, TlsError &err); int pending(const_session_t session); void shutdown(session_t session, bool graceful); // Connection state bool is_peer_closed(session_t session, socket_t sock); // Certificate verification cert_t get_peer_cert(const_session_t session); void free_cert(cert_t cert); bool verify_hostname(cert_t cert, const char *hostname); uint64_t hostname_mismatch_code(); long get_verify_result(const_session_t session); // Certificate introspection std::string get_cert_subject_cn(cert_t cert); std::string get_cert_issuer_name(cert_t cert); bool get_cert_sans(cert_t cert, std::vector &sans); bool get_cert_validity(cert_t cert, time_t ¬_before, time_t ¬_after); std::string get_cert_serial(cert_t cert); bool get_cert_der(cert_t cert, std::vector &der); const char *get_sni(const_session_t session); // CA store management ca_store_t create_ca_store(const char *pem, size_t len); void free_ca_store(ca_store_t store); bool set_ca_store(ctx_t ctx, ca_store_t store); size_t get_ca_certs(ctx_t ctx, std::vector &certs); std::vector get_ca_names(ctx_t ctx); // Dynamic certificate update (for servers) bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem, const char *password); bool update_server_client_ca(ctx_t ctx, const char *ca_pem); // Certificate verification callback bool set_verify_callback(ctx_t ctx, VerifyCallback callback); long get_verify_error(const_session_t session); std::string verify_error_string(long error_code); // TlsError information uint64_t peek_error(); uint64_t get_error(); std::string error_string(uint64_t code); } // namespace tls #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 1: detail namespace - Non-SSL utilities */ namespace detail { bool set_socket_opt_impl(socket_t sock, int level, int optname, const void *optval, socklen_t optlen) { return setsockopt(sock, level, optname, #ifdef _WIN32 reinterpret_cast(optval), #else optval, #endif optlen) == 0; } bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec, time_t usec) { #ifdef _WIN32 auto timeout = static_cast(sec * 1000 + usec / 1000); #else timeval timeout; timeout.tv_sec = static_cast(sec); timeout.tv_usec = static_cast(usec); #endif return set_socket_opt_impl(sock, level, optname, &timeout, sizeof(timeout)); } bool is_hex(char c, int &v) { if (isdigit(static_cast(c))) { v = c - '0'; return true; } else if ('A' <= c && c <= 'F') { v = c - 'A' + 10; return true; } else if ('a' <= c && c <= 'f') { v = c - 'a' + 10; return true; } return false; } bool from_hex_to_i(const std::string &s, size_t i, size_t cnt, int &val) { if (i >= s.size()) { return false; } val = 0; for (; cnt; i++, cnt--) { if (!s[i]) { return false; } auto v = 0; if (is_hex(s[i], v)) { val = val * 16 + v; } else { return false; } } return true; } std::string from_i_to_hex(size_t n) { static const auto charset = "0123456789abcdef"; std::string ret; do { ret = charset[n & 15] + ret; n >>= 4; } while (n > 0); return ret; } std::string compute_etag(const FileStat &fs) { if (!fs.is_file()) { return std::string(); } // If mtime cannot be determined (negative value indicates an error // or sentinel), do not generate an ETag. Returning a neutral / fixed // value like 0 could collide with a real file that legitimately has // mtime == 0 (epoch) and lead to misleading validators. auto mtime_raw = fs.mtime(); if (mtime_raw < 0) { return std::string(); } auto mtime = static_cast(mtime_raw); auto size = fs.size(); return std::string("W/\"") + from_i_to_hex(mtime) + "-" + from_i_to_hex(size) + "\""; } // Format time_t as HTTP-date (RFC 9110 Section 5.6.7): "Sun, 06 Nov 1994 // 08:49:37 GMT" This implementation is defensive: it validates `mtime`, checks // return values from `gmtime_r`/`gmtime_s`, and ensures `strftime` succeeds. std::string file_mtime_to_http_date(time_t mtime) { if (mtime < 0) { return std::string(); } struct tm tm_buf; #ifdef _WIN32 if (gmtime_s(&tm_buf, &mtime) != 0) { return std::string(); } #else if (gmtime_r(&mtime, &tm_buf) == nullptr) { return std::string(); } #endif char buf[64]; if (strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf) == 0) { return std::string(); } return std::string(buf); } // Parse HTTP-date (RFC 9110 Section 5.6.7) to time_t. Returns -1 on failure. time_t parse_http_date(const std::string &date_str) { struct tm tm_buf; // Create a classic locale object once for all parsing attempts const std::locale classic_locale = std::locale::classic(); // Try to parse using std::get_time (C++11, cross-platform) auto try_parse = [&](const char *fmt) -> bool { std::istringstream ss(date_str); ss.imbue(classic_locale); memset(&tm_buf, 0, sizeof(tm_buf)); ss >> std::get_time(&tm_buf, fmt); return !ss.fail(); }; // RFC 9110 preferred format (HTTP-date): "Sun, 06 Nov 1994 08:49:37 GMT" if (!try_parse("%a, %d %b %Y %H:%M:%S")) { // RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT" if (!try_parse("%A, %d-%b-%y %H:%M:%S")) { // asctime format: "Sun Nov 6 08:49:37 1994" if (!try_parse("%a %b %d %H:%M:%S %Y")) { return static_cast(-1); } } } #ifdef _WIN32 return _mkgmtime(&tm_buf); #elif defined _AIX return mktime(&tm_buf); #else return timegm(&tm_buf); #endif } bool is_weak_etag(const std::string &s) { // Check if the string is a weak ETag (starts with 'W/"') return s.size() > 3 && s[0] == 'W' && s[1] == '/' && s[2] == '"'; } bool is_strong_etag(const std::string &s) { // Check if the string is a strong ETag (starts and ends with '"', at least 2 // chars) return s.size() >= 2 && s[0] == '"' && s.back() == '"'; } size_t to_utf8(int code, char *buff) { if (code < 0x0080) { buff[0] = static_cast(code & 0x7F); return 1; } else if (code < 0x0800) { buff[0] = static_cast(0xC0 | ((code >> 6) & 0x1F)); buff[1] = static_cast(0x80 | (code & 0x3F)); return 2; } else if (code < 0xD800) { buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); buff[2] = static_cast(0x80 | (code & 0x3F)); return 3; } else if (code < 0xE000) { // D800 - DFFF is invalid... return 0; } else if (code < 0x10000) { buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); buff[2] = static_cast(0x80 | (code & 0x3F)); return 3; } else if (code < 0x110000) { buff[0] = static_cast(0xF0 | ((code >> 18) & 0x7)); buff[1] = static_cast(0x80 | ((code >> 12) & 0x3F)); buff[2] = static_cast(0x80 | ((code >> 6) & 0x3F)); buff[3] = static_cast(0x80 | (code & 0x3F)); return 4; } // NOTREACHED return 0; } } // namespace detail namespace ws { namespace impl { bool is_valid_utf8(const std::string &s) { size_t i = 0; auto n = s.size(); while (i < n) { auto c = static_cast(s[i]); size_t len; uint32_t cp; if (c < 0x80) { i++; continue; } else if ((c & 0xE0) == 0xC0) { len = 2; cp = c & 0x1F; } else if ((c & 0xF0) == 0xE0) { len = 3; cp = c & 0x0F; } else if ((c & 0xF8) == 0xF0) { len = 4; cp = c & 0x07; } else { return false; } if (i + len > n) { return false; } for (size_t j = 1; j < len; j++) { auto b = static_cast(s[i + j]); if ((b & 0xC0) != 0x80) { return false; } cp = (cp << 6) | (b & 0x3F); } // Overlong encoding check if (len == 2 && cp < 0x80) { return false; } if (len == 3 && cp < 0x800) { return false; } if (len == 4 && cp < 0x10000) { return false; } // Surrogate halves (U+D800..U+DFFF) and beyond U+10FFFF are invalid if (cp >= 0xD800 && cp <= 0xDFFF) { return false; } if (cp > 0x10FFFF) { return false; } i += len; } return true; } } // namespace impl } // namespace ws namespace detail { // NOTE: This code came up with the following stackoverflow post: // https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c std::string base64_encode(const std::string &in) { static const auto lookup = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::string out; out.reserve(in.size()); auto val = 0; auto valb = -6; for (auto c : in) { val = (val << 8) + static_cast(c); valb += 8; while (valb >= 0) { out.push_back(lookup[(val >> valb) & 0x3F]); valb -= 6; } } if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); } while (out.size() % 4) { out.push_back('='); } return out; } std::string sha1(const std::string &input) { // RFC 3174 SHA-1 implementation auto left_rotate = [](uint32_t x, uint32_t n) -> uint32_t { return (x << n) | (x >> (32 - n)); }; uint32_t h0 = 0x67452301; uint32_t h1 = 0xEFCDAB89; uint32_t h2 = 0x98BADCFE; uint32_t h3 = 0x10325476; uint32_t h4 = 0xC3D2E1F0; // Pre-processing: adding padding bits std::string msg = input; uint64_t original_bit_len = static_cast(msg.size()) * 8; msg.push_back(static_cast(0x80u)); while (msg.size() % 64 != 56) { msg.push_back(0); } // Append original length in bits as 64-bit big-endian for (int i = 56; i >= 0; i -= 8) { msg.push_back(static_cast((original_bit_len >> i) & 0xFF)); } // Process each 512-bit chunk for (size_t offset = 0; offset < msg.size(); offset += 64) { uint32_t w[80]; for (size_t i = 0; i < 16; i++) { w[i] = (static_cast(static_cast(msg[offset + i * 4])) << 24) | (static_cast(static_cast(msg[offset + i * 4 + 1])) << 16) | (static_cast(static_cast(msg[offset + i * 4 + 2])) << 8) | (static_cast( static_cast(msg[offset + i * 4 + 3]))); } for (int i = 16; i < 80; i++) { w[i] = left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); } uint32_t a = h0, b = h1, c = h2, d = h3, e = h4; for (int i = 0; i < 80; i++) { uint32_t f, k; if (i < 20) { f = (b & c) | ((~b) & d); k = 0x5A827999; } else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } else { f = b ^ c ^ d; k = 0xCA62C1D6; } uint32_t temp = left_rotate(a, 5) + f + e + k + w[i]; e = d; d = c; c = left_rotate(b, 30); b = a; a = temp; } h0 += a; h1 += b; h2 += c; h3 += d; h4 += e; } // Produce the final hash as a 20-byte binary string std::string hash(20, '\0'); for (size_t i = 0; i < 4; i++) { hash[i] = static_cast((h0 >> (24 - i * 8)) & 0xFF); hash[4 + i] = static_cast((h1 >> (24 - i * 8)) & 0xFF); hash[8 + i] = static_cast((h2 >> (24 - i * 8)) & 0xFF); hash[12 + i] = static_cast((h3 >> (24 - i * 8)) & 0xFF); hash[16 + i] = static_cast((h4 >> (24 - i * 8)) & 0xFF); } return hash; } std::string websocket_accept_key(const std::string &client_key) { const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; return base64_encode(sha1(client_key + magic)); } bool is_websocket_upgrade(const Request &req) { if (req.method != "GET") { return false; } // Check Upgrade: websocket (case-insensitive) auto upgrade_it = req.headers.find("Upgrade"); if (upgrade_it == req.headers.end()) { return false; } auto upgrade_val = case_ignore::to_lower(upgrade_it->second); if (upgrade_val != "websocket") { return false; } // Check Connection header contains "Upgrade" auto connection_it = req.headers.find("Connection"); if (connection_it == req.headers.end()) { return false; } auto connection_val = case_ignore::to_lower(connection_it->second); if (connection_val.find("upgrade") == std::string::npos) { return false; } // Check Sec-WebSocket-Key is a valid base64-encoded 16-byte value (24 chars) // RFC 6455 Section 4.2.1 auto ws_key = req.get_header_value("Sec-WebSocket-Key"); if (ws_key.size() != 24 || ws_key[22] != '=' || ws_key[23] != '=') { return false; } static const std::string b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; for (size_t i = 0; i < 22; i++) { if (b64chars.find(ws_key[i]) == std::string::npos) { return false; } } // Check Sec-WebSocket-Version: 13 auto version = req.get_header_value("Sec-WebSocket-Version"); if (version != "13") { return false; } return true; } bool write_websocket_frame(Stream &strm, ws::Opcode opcode, const char *data, size_t len, bool fin, bool mask) { // First byte: FIN + opcode uint8_t header[2]; header[0] = static_cast((fin ? 0x80 : 0x00) | (static_cast(opcode) & 0x0F)); // Second byte: MASK + payload length if (len < 126) { header[1] = static_cast(len); if (mask) { header[1] |= 0x80; } if (strm.write(reinterpret_cast(header), 2) < 0) { return false; } } else if (len <= 0xFFFF) { header[1] = 126; if (mask) { header[1] |= 0x80; } if (strm.write(reinterpret_cast(header), 2) < 0) { return false; } uint8_t ext[2]; ext[0] = static_cast((len >> 8) & 0xFF); ext[1] = static_cast(len & 0xFF); if (strm.write(reinterpret_cast(ext), 2) < 0) { return false; } } else { header[1] = 127; if (mask) { header[1] |= 0x80; } if (strm.write(reinterpret_cast(header), 2) < 0) { return false; } uint8_t ext[8]; for (int i = 7; i >= 0; i--) { ext[7 - i] = static_cast((static_cast(len) >> (i * 8)) & 0xFF); } if (strm.write(reinterpret_cast(ext), 8) < 0) { return false; } } if (mask) { // Generate random mask key thread_local std::mt19937 rng(std::random_device{}()); uint8_t mask_key[4]; auto r = rng(); std::memcpy(mask_key, &r, 4); if (strm.write(reinterpret_cast(mask_key), 4) < 0) { return false; } // Write masked payload in chunks const size_t chunk_size = 4096; std::vector buf((std::min)(len, chunk_size)); for (size_t offset = 0; offset < len; offset += chunk_size) { size_t n = (std::min)(chunk_size, len - offset); for (size_t i = 0; i < n; i++) { buf[i] = data[offset + i] ^ static_cast(mask_key[(offset + i) % 4]); } if (strm.write(buf.data(), n) < 0) { return false; } } } else { if (len > 0) { if (strm.write(data, len) < 0) { return false; } } } return true; } } // namespace detail namespace ws { namespace impl { bool read_websocket_frame(Stream &strm, Opcode &opcode, std::string &payload, bool &fin, bool expect_masked, size_t max_len) { // Read first 2 bytes uint8_t header[2]; if (strm.read(reinterpret_cast(header), 2) != 2) { return false; } fin = (header[0] & 0x80) != 0; // RSV1, RSV2, RSV3 must be 0 when no extension is negotiated if (header[0] & 0x70) { return false; } opcode = static_cast(header[0] & 0x0F); bool masked = (header[1] & 0x80) != 0; uint64_t payload_len = header[1] & 0x7F; // RFC 6455 Section 5.5: control frames MUST NOT be fragmented and // MUST have a payload length of 125 bytes or less bool is_control = (static_cast(opcode) & 0x08) != 0; if (is_control) { if (!fin) { return false; } if (payload_len > 125) { return false; } } if (masked != expect_masked) { return false; } // Extended payload length if (payload_len == 126) { uint8_t ext[2]; if (strm.read(reinterpret_cast(ext), 2) != 2) { return false; } payload_len = (static_cast(ext[0]) << 8) | ext[1]; } else if (payload_len == 127) { uint8_t ext[8]; if (strm.read(reinterpret_cast(ext), 8) != 8) { return false; } // RFC 6455 Section 5.2: the most significant bit MUST be 0 if (ext[0] & 0x80) { return false; } payload_len = 0; for (int i = 0; i < 8; i++) { payload_len = (payload_len << 8) | ext[i]; } } if (payload_len > max_len) { return false; } // Read mask key if present uint8_t mask_key[4] = {0}; if (masked) { if (strm.read(reinterpret_cast(mask_key), 4) != 4) { return false; } } // Read payload payload.resize(static_cast(payload_len)); if (payload_len > 0) { size_t total_read = 0; while (total_read < payload_len) { auto n = strm.read(&payload[total_read], static_cast(payload_len - total_read)); if (n <= 0) { return false; } total_read += static_cast(n); } } // Unmask if needed if (masked) { for (size_t i = 0; i < payload.size(); i++) { payload[i] ^= static_cast(mask_key[i % 4]); } } return true; } } // namespace impl } // namespace ws namespace detail { bool is_valid_path(const std::string &path) { size_t level = 0; size_t i = 0; // Skip slash while (i < path.size() && path[i] == '/') { i++; } 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; 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++; } } return true; } bool canonicalize_path(const char *path, std::string &resolved) { #if defined(_WIN32) char buf[_MAX_PATH]; if (_fullpath(buf, path, _MAX_PATH) == nullptr) { return false; } resolved = buf; #elif defined(PATH_MAX) char buf[PATH_MAX]; if (realpath(path, buf) == nullptr) { return false; } resolved = buf; #else auto buf = realpath(path, nullptr); auto guard = scope_exit([&]() { std::free(buf); }); if (buf == nullptr) { return false; } resolved = buf; #endif return true; } bool is_path_within_base(const std::string &resolved_path, const std::string &resolved_base) { #if defined(_WIN32) return _strnicmp(resolved_path.c_str(), resolved_base.c_str(), resolved_base.size()) == 0; #else return strncmp(resolved_path.c_str(), resolved_base.c_str(), resolved_base.size()) == 0; #endif } FileStat::FileStat(const std::string &path) { #if defined(_WIN32) auto wpath = u8string_to_wstring(path.c_str()); ret_ = _wstat(wpath.c_str(), &st_); #else ret_ = stat(path.c_str(), &st_); #endif } bool FileStat::is_file() const { return ret_ >= 0 && S_ISREG(st_.st_mode); } bool FileStat::is_dir() const { return ret_ >= 0 && S_ISDIR(st_.st_mode); } time_t FileStat::mtime() const { return ret_ >= 0 ? static_cast(st_.st_mtime) : static_cast(-1); } size_t FileStat::size() const { return ret_ >= 0 ? static_cast(st_.st_size) : 0; } std::string encode_path(const std::string &s) { std::string result; result.reserve(s.size()); for (size_t i = 0; s[i]; i++) { switch (s[i]) { case ' ': result += "%20"; break; case '+': result += "%2B"; break; case '\r': result += "%0D"; break; case '\n': result += "%0A"; break; case '\'': result += "%27"; break; case ',': result += "%2C"; break; // case ':': result += "%3A"; break; // ok? probably... case ';': result += "%3B"; break; default: auto c = static_cast(s[i]); if (c >= 0x80) { result += '%'; char hex[4]; auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c); assert(len == 2); result.append(hex, static_cast(len)); } else { result += s[i]; } break; } } return result; } std::string file_extension(const std::string &path) { std::smatch m; thread_local auto re = std::regex("\\.([a-zA-Z0-9]+)$"); if (std::regex_search(path, m, re)) { return m[1].str(); } return std::string(); } bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; } template bool parse_header(const char *beg, const char *end, T fn); template bool parse_header(const char *beg, const char *end, T fn) { // Skip trailing spaces and tabs. while (beg < end && is_space_or_tab(end[-1])) { end--; } auto p = beg; while (p < end && *p != ':') { p++; } auto name = std::string(beg, p); if (!detail::fields::is_field_name(name)) { return false; } if (p == end) { return false; } auto key_end = p; if (*p++ != ':') { return false; } while (p < end && is_space_or_tab(*p)) { p++; } if (p <= end) { auto key_len = key_end - beg; if (!key_len) { return false; } auto key = std::string(beg, key_end); auto val = std::string(p, end); if (!detail::fields::is_field_value(val)) { return false; } // RFC 9110 ยง5.5: header field values are opaque octets and MUST NOT be // percent-decoded by the recipient. Applications that need to interpret a // value as a URI component should call httplib::decode_uri_component() // (or decode_path_component()) explicitly. fn(key, val); return true; } return false; } bool parse_trailers(stream_line_reader &line_reader, Headers &dest, const Headers &src_headers) { // NOTE: In RFC 9112, '7.1 Chunked Transfer Coding' mentions "The chunked // transfer coding is complete when a chunk with a chunk-size of zero is // received, possibly followed by a trailer section, and finally terminated by // an empty line". https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1 // // In '7.1.3. Decoding Chunked', however, the pseudo-code in the section // doesn't care for the existence of the final CRLF. In other words, it seems // to be ok whether the final CRLF exists or not in the chunked data. // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1.3 // // According to the reference code in RFC 9112, cpp-httplib now allows // chunked transfer coding data without the final CRLF. // RFC 7230 Section 4.1.2 - Headers prohibited in trailers thread_local case_ignore::unordered_set prohibited_trailers = { "transfer-encoding", "content-length", "host", "authorization", "www-authenticate", "proxy-authenticate", "proxy-authorization", "cookie", "set-cookie", "cache-control", "expect", "max-forwards", "pragma", "range", "te", "age", "expires", "date", "location", "retry-after", "vary", "warning", "content-encoding", "content-type", "content-range", "trailer"}; case_ignore::unordered_set declared_trailers; auto trailer_header = get_header_value(src_headers, "Trailer", "", 0); if (trailer_header && std::strlen(trailer_header)) { auto len = std::strlen(trailer_header); split(trailer_header, trailer_header + len, ',', [&](const char *b, const char *e) { const char *kbeg = b; const char *kend = e; while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) { ++kbeg; } while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) { --kend; } std::string key(kbeg, static_cast(kend - kbeg)); if (!key.empty() && prohibited_trailers.find(key) == prohibited_trailers.end()) { declared_trailers.insert(key); } }); } size_t trailer_header_count = 0; while (strcmp(line_reader.ptr(), "\r\n") != 0) { if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } constexpr auto line_terminator_len = 2; auto line_beg = line_reader.ptr(); auto line_end = line_reader.ptr() + line_reader.size() - line_terminator_len; if (!parse_header(line_beg, line_end, [&](const std::string &key, const std::string &val) { if (declared_trailers.find(key) != declared_trailers.end()) { dest.emplace(key, val); trailer_header_count++; } })) { return false; } if (!line_reader.getline()) { return false; } } return true; } std::pair trim(const char *b, const char *e, size_t left, size_t right) { while (b + left < e && is_space_or_tab(b[left])) { left++; } while (right > 0 && is_space_or_tab(b[right - 1])) { right--; } return std::make_pair(left, right); } std::string trim_copy(const std::string &s) { auto r = trim(s.data(), s.data() + s.size(), 0, s.size()); return s.substr(r.first, r.second - r.first); } std::string trim_double_quotes_copy(const std::string &s) { if (s.length() >= 2 && s.front() == '"' && s.back() == '"') { return s.substr(1, s.size() - 2); } return s; } void divide(const char *data, std::size_t size, char d, std::function fn) { const auto it = std::find(data, data + size, d); const auto found = static_cast(it != data + size); const auto lhs_data = data; const auto lhs_size = static_cast(it - data); const auto rhs_data = it + found; const auto rhs_size = size - lhs_size - found; fn(lhs_data, lhs_size, rhs_data, rhs_size); } void divide(const std::string &str, char d, std::function fn) { divide(str.data(), str.size(), d, std::move(fn)); } void split(const char *b, const char *e, char d, std::function fn) { return split(b, e, d, (std::numeric_limits::max)(), std::move(fn)); } void split(const char *b, const char *e, char d, size_t m, std::function fn) { size_t i = 0; size_t beg = 0; size_t count = 1; while (e ? (b + i < e) : (b[i] != '\0')) { if (b[i] == d && count < m) { auto r = trim(b, e, beg, i); if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } beg = i + 1; count++; } i++; } if (i) { auto r = trim(b, e, beg, i); if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } } } bool split_find(const char *b, const char *e, char d, size_t m, std::function fn) { size_t i = 0; size_t beg = 0; size_t count = 1; while (e ? (b + i < e) : (b[i] != '\0')) { if (b[i] == d && count < m) { auto r = trim(b, e, beg, i); if (r.first < r.second) { auto found = fn(&b[r.first], &b[r.second]); if (found) { return true; } } beg = i + 1; count++; } i++; } if (i) { auto r = trim(b, e, beg, i); if (r.first < r.second) { auto found = fn(&b[r.first], &b[r.second]); if (found) { return true; } } } return false; } bool split_find(const char *b, const char *e, char d, std::function fn) { return split_find(b, e, d, (std::numeric_limits::max)(), std::move(fn)); } stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer, size_t fixed_buffer_size) : strm_(strm), fixed_buffer_(fixed_buffer), fixed_buffer_size_(fixed_buffer_size) {} const char *stream_line_reader::ptr() const { if (growable_buffer_.empty()) { return fixed_buffer_; } else { return growable_buffer_.data(); } } size_t stream_line_reader::size() const { if (growable_buffer_.empty()) { return fixed_buffer_used_size_; } else { return growable_buffer_.size(); } } bool stream_line_reader::end_with_crlf() const { auto end = ptr() + size(); return size() >= 2 && end[-2] == '\r' && end[-1] == '\n'; } bool stream_line_reader::getline() { fixed_buffer_used_size_ = 0; growable_buffer_.clear(); #ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR char prev_byte = 0; #endif for (size_t i = 0;; i++) { if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) { // Treat exceptionally long lines as an error to // prevent infinite loops/memory exhaustion return false; } char byte; auto n = strm_.read(&byte, 1); if (n < 0) { return false; } else if (n == 0) { if (i == 0) { return false; } else { break; } } append(byte); #ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR if (byte == '\n') { break; } #else if (prev_byte == '\r' && byte == '\n') { break; } prev_byte = byte; #endif } return true; } void stream_line_reader::append(char c) { if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { fixed_buffer_[fixed_buffer_used_size_++] = c; fixed_buffer_[fixed_buffer_used_size_] = '\0'; } else { if (growable_buffer_.empty()) { assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); } growable_buffer_ += c; } } mmap::mmap(const char *path) { open(path); } mmap::~mmap() { close(); } bool mmap::open(const char *path) { close(); #if defined(_WIN32) auto wpath = u8string_to_wstring(path); if (wpath.empty()) { return false; } hFile_ = ::CreateFile2(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, NULL); if (hFile_ == INVALID_HANDLE_VALUE) { return false; } LARGE_INTEGER size{}; if (!::GetFileSizeEx(hFile_, &size)) { return false; } // If the following line doesn't compile due to QuadPart, update Windows SDK. // See: // https://github.com/yhirose/cpp-httplib/issues/1903#issuecomment-2316520721 if (static_cast(size.QuadPart) > (std::numeric_limits::max)()) { // `size_t` might be 32-bits, on 32-bits Windows. return false; } size_ = static_cast(size.QuadPart); hMapping_ = ::CreateFileMappingFromApp(hFile_, NULL, PAGE_READONLY, size_, NULL); // Special treatment for an empty file... if (hMapping_ == NULL && size_ == 0) { close(); is_open_empty_file = true; return true; } if (hMapping_ == NULL) { close(); return false; } addr_ = ::MapViewOfFileFromApp(hMapping_, FILE_MAP_READ, 0, 0); if (addr_ == nullptr) { close(); return false; } #else fd_ = ::open(path, O_RDONLY); if (fd_ == -1) { return false; } struct stat sb; if (fstat(fd_, &sb) == -1) { close(); return false; } size_ = static_cast(sb.st_size); addr_ = ::mmap(NULL, size_, PROT_READ, MAP_PRIVATE, fd_, 0); // Special treatment for an empty file... if (addr_ == MAP_FAILED && size_ == 0) { close(); is_open_empty_file = true; return false; } #endif return true; } bool mmap::is_open() const { return is_open_empty_file ? true : addr_ != nullptr; } size_t mmap::size() const { return size_; } const char *mmap::data() const { return is_open_empty_file ? "" : static_cast(addr_); } void mmap::close() { #if defined(_WIN32) if (addr_) { ::UnmapViewOfFile(addr_); addr_ = nullptr; } if (hMapping_) { ::CloseHandle(hMapping_); hMapping_ = NULL; } if (hFile_ != INVALID_HANDLE_VALUE) { ::CloseHandle(hFile_); hFile_ = INVALID_HANDLE_VALUE; } is_open_empty_file = false; #else if (addr_ != nullptr) { munmap(addr_, size_); addr_ = nullptr; } if (fd_ != -1) { ::close(fd_); fd_ = -1; } #endif size_ = 0; } int close_socket(socket_t sock) noexcept { #ifdef _WIN32 return closesocket(sock); #else return close(sock); #endif } template ssize_t handle_EINTR(T fn) { ssize_t res = 0; while (true) { res = fn(); if (res < 0 && errno == EINTR) { std::this_thread::sleep_for(std::chrono::microseconds{1}); continue; } break; } return res; } ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags) { return handle_EINTR([&]() { return recv(sock, #ifdef _WIN32 static_cast(ptr), static_cast(size), #else ptr, size, #endif flags); }); } ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags) { return handle_EINTR([&]() { return send(sock, #ifdef _WIN32 static_cast(ptr), static_cast(size), #else ptr, size, #endif flags); }); } int poll_wrapper(struct pollfd *fds, nfds_t nfds, int timeout) { #ifdef _WIN32 return ::WSAPoll(fds, nfds, timeout); #else return ::poll(fds, nfds, timeout); #endif } ssize_t select_impl(socket_t sock, short events, time_t sec, time_t usec) { struct pollfd pfd; pfd.fd = sock; pfd.events = events; pfd.revents = 0; auto timeout = static_cast(sec * 1000 + usec / 1000); return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); }); } ssize_t select_read(socket_t sock, time_t sec, time_t usec) { return select_impl(sock, POLLIN, sec, usec); } ssize_t select_write(socket_t sock, time_t sec, time_t usec) { return select_impl(sock, POLLOUT, sec, usec); } Error wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) { struct pollfd pfd_read; pfd_read.fd = sock; pfd_read.events = POLLIN | POLLOUT; pfd_read.revents = 0; auto timeout = static_cast(sec * 1000 + usec / 1000); auto poll_res = handle_EINTR([&]() { return poll_wrapper(&pfd_read, 1, timeout); }); if (poll_res == 0) { return Error::ConnectionTimeout; } if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) { auto error = 0; socklen_t len = sizeof(error); auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &len); auto successful = res >= 0 && !error; return successful ? Error::Success : Error::Connection; } return Error::Connection; } bool is_socket_alive(socket_t sock) { const auto val = detail::select_read(sock, 0, 0); if (val == 0) { return true; } else if (val < 0 && errno == EBADF) { return false; } char buf[1]; return detail::read_socket(sock, &buf[0], sizeof(buf), MSG_PEEK) > 0; } class SocketStream final : public Stream { public: SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec = 0, std::chrono::time_point start_time = (std::chrono::steady_clock::time_point::min)()); ~SocketStream() override; bool is_readable() const override; bool wait_readable() const override; bool wait_writable() const override; bool is_peer_alive() const override; ssize_t read(char *ptr, size_t size) override; ssize_t write(const char *ptr, size_t size) override; void get_remote_ip_and_port(std::string &ip, int &port) const override; void get_local_ip_and_port(std::string &ip, int &port) const override; socket_t socket() const override; time_t duration() const override; void set_read_timeout(time_t sec, time_t usec = 0) override; private: socket_t sock_; time_t read_timeout_sec_; time_t read_timeout_usec_; time_t write_timeout_sec_; time_t write_timeout_usec_; time_t max_timeout_msec_; const std::chrono::time_point start_time_; std::vector read_buff_; size_t read_buff_off_ = 0; size_t read_buff_content_size_ = 0; static const size_t read_buff_size_ = 1024l * 4; }; bool keep_alive(const std::atomic &svr_sock, socket_t sock, time_t keep_alive_timeout_sec) { using namespace std::chrono; const auto interval_usec = CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND; // Avoid expensive `steady_clock::now()` call for the first time if (select_read(sock, 0, interval_usec) > 0) { return true; } const auto start = steady_clock::now() - microseconds{interval_usec}; const auto timeout = seconds{keep_alive_timeout_sec}; while (true) { if (svr_sock == INVALID_SOCKET) { break; // Server socket is closed } auto val = select_read(sock, 0, interval_usec); if (val < 0) { break; // Ssocket error } else if (val == 0) { if (steady_clock::now() - start > timeout) { break; // Timeout } } else { return true; // Ready for read } } return false; } template bool process_server_socket_core(const std::atomic &svr_sock, socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, T callback) { assert(keep_alive_max_count > 0); auto ret = false; auto count = keep_alive_max_count; while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) { auto close_connection = count == 1; auto connection_closed = false; ret = callback(close_connection, connection_closed); if (!ret || connection_closed) { break; } count--; } return ret; } template bool process_server_socket(const std::atomic &svr_sock, socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, T callback) { return process_server_socket_core( svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, [&](bool close_connection, bool &connection_closed) { SocketStream strm(sock, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); return callback(strm, close_connection, connection_closed); }); } bool process_client_socket( socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, std::chrono::time_point start_time, std::function callback) { SocketStream strm(sock, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec, max_timeout_msec, start_time); return callback(strm); } int shutdown_socket(socket_t sock) noexcept { #ifdef _WIN32 return shutdown(sock, SD_BOTH); #else return shutdown(sock, SHUT_RDWR); #endif } std::string escape_abstract_namespace_unix_domain(const std::string &s) { if (s.size() > 1 && s[0] == '\0') { auto ret = s; ret[0] = '@'; return ret; } return s; } std::string unescape_abstract_namespace_unix_domain(const std::string &s) { if (s.size() > 1 && s[0] == '@') { auto ret = s; ret[0] = '\0'; return ret; } return s; } int getaddrinfo_with_timeout(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res, time_t timeout_sec) { #ifdef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO if (timeout_sec <= 0) { // No timeout specified, use standard getaddrinfo return getaddrinfo(node, service, hints, res); } #ifdef _WIN32 // Windows-specific implementation using GetAddrInfoEx with overlapped I/O OVERLAPPED overlapped = {}; HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr); if (!event) { return EAI_FAIL; } overlapped.hEvent = event; PADDRINFOEXW result_addrinfo = nullptr; HANDLE cancel_handle = nullptr; ADDRINFOEXW hints_ex = {}; if (hints) { hints_ex.ai_flags = hints->ai_flags; hints_ex.ai_family = hints->ai_family; hints_ex.ai_socktype = hints->ai_socktype; hints_ex.ai_protocol = hints->ai_protocol; } auto wnode = u8string_to_wstring(node); auto wservice = u8string_to_wstring(service); auto ret = ::GetAddrInfoExW(wnode.data(), wservice.data(), NS_DNS, nullptr, hints ? &hints_ex : nullptr, &result_addrinfo, nullptr, &overlapped, nullptr, &cancel_handle); if (ret == WSA_IO_PENDING) { auto wait_result = ::WaitForSingleObject(event, static_cast(timeout_sec * 1000)); if (wait_result == WAIT_TIMEOUT) { if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); } ::CloseHandle(event); return EAI_AGAIN; } DWORD bytes_returned; if (!::GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped, &bytes_returned, FALSE)) { ::CloseHandle(event); return ::WSAGetLastError(); } } ::CloseHandle(event); if (ret == NO_ERROR || ret == WSA_IO_PENDING) { *res = reinterpret_cast(result_addrinfo); return 0; } return ret; #elif TARGET_OS_MAC && defined(__clang__) if (!node) { return EAI_NONAME; } // macOS implementation using CFHost API for asynchronous DNS resolution CFStringRef hostname_ref = CFStringCreateWithCString( kCFAllocatorDefault, node, kCFStringEncodingUTF8); if (!hostname_ref) { return EAI_MEMORY; } CFHostRef host_ref = CFHostCreateWithName(kCFAllocatorDefault, hostname_ref); CFRelease(hostname_ref); if (!host_ref) { return EAI_MEMORY; } // Set up context for callback struct CFHostContext { bool completed = false; bool success = false; CFArrayRef addresses = nullptr; std::mutex mutex; std::condition_variable cv; } context; CFHostClientContext client_context; memset(&client_context, 0, sizeof(client_context)); client_context.info = &context; // Set callback auto callback = [](CFHostRef theHost, CFHostInfoType /*typeInfo*/, const CFStreamError *error, void *info) { auto ctx = static_cast(info); std::lock_guard lock(ctx->mutex); if (error && error->error != 0) { ctx->success = false; } else { Boolean hasBeenResolved; ctx->addresses = CFHostGetAddressing(theHost, &hasBeenResolved); if (ctx->addresses && hasBeenResolved) { CFRetain(ctx->addresses); ctx->success = true; } else { ctx->success = false; } } ctx->completed = true; ctx->cv.notify_one(); }; if (!CFHostSetClient(host_ref, callback, &client_context)) { CFRelease(host_ref); return EAI_SYSTEM; } // Schedule on run loop CFRunLoopRef run_loop = CFRunLoopGetCurrent(); CFHostScheduleWithRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); // Start resolution CFStreamError stream_error; if (!CFHostStartInfoResolution(host_ref, kCFHostAddresses, &stream_error)) { CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); CFRelease(host_ref); return EAI_FAIL; } // Wait for completion with timeout auto timeout_time = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec); bool timed_out = false; { std::unique_lock lock(context.mutex); while (!context.completed) { auto now = std::chrono::steady_clock::now(); if (now >= timeout_time) { timed_out = true; break; } // Run the runloop for a short time lock.unlock(); CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true); lock.lock(); } } // Clean up CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); CFHostSetClient(host_ref, nullptr, nullptr); if (timed_out || !context.completed) { CFHostCancelInfoResolution(host_ref, kCFHostAddresses); CFRelease(host_ref); return EAI_AGAIN; } if (!context.success || !context.addresses) { CFRelease(host_ref); return EAI_NODATA; } // Convert CFArray to addrinfo CFIndex count = CFArrayGetCount(context.addresses); if (count == 0) { CFRelease(context.addresses); CFRelease(host_ref); return EAI_NODATA; } struct addrinfo *result_addrinfo = nullptr; struct addrinfo **current = &result_addrinfo; for (CFIndex i = 0; i < count; i++) { CFDataRef addr_data = static_cast(CFArrayGetValueAtIndex(context.addresses, i)); if (!addr_data) continue; const struct sockaddr *sockaddr_ptr = reinterpret_cast(CFDataGetBytePtr(addr_data)); socklen_t sockaddr_len = static_cast(CFDataGetLength(addr_data)); // Allocate addrinfo structure *current = static_cast(malloc(sizeof(struct addrinfo))); if (!*current) { freeaddrinfo(result_addrinfo); CFRelease(context.addresses); CFRelease(host_ref); return EAI_MEMORY; } memset(*current, 0, sizeof(struct addrinfo)); // Set up addrinfo fields (*current)->ai_family = sockaddr_ptr->sa_family; (*current)->ai_socktype = hints ? hints->ai_socktype : SOCK_STREAM; (*current)->ai_protocol = hints ? hints->ai_protocol : IPPROTO_TCP; (*current)->ai_addrlen = sockaddr_len; // Copy sockaddr (*current)->ai_addr = static_cast(malloc(sockaddr_len)); if (!(*current)->ai_addr) { freeaddrinfo(result_addrinfo); CFRelease(context.addresses); CFRelease(host_ref); return EAI_MEMORY; } memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len); // Set port if service is specified if (service && *service) { int port = 0; if (parse_port(service, strlen(service), port)) { if (sockaddr_ptr->sa_family == AF_INET) { reinterpret_cast((*current)->ai_addr) ->sin_port = htons(static_cast(port)); } else if (sockaddr_ptr->sa_family == AF_INET6) { reinterpret_cast((*current)->ai_addr) ->sin6_port = htons(static_cast(port)); } } } current = &((*current)->ai_next); } CFRelease(context.addresses); CFRelease(host_ref); *res = result_addrinfo; return 0; #elif defined(_GNU_SOURCE) && defined(__GLIBC__) && \ (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2)) // #2431: gai_cancel() is non-blocking and may return EAI_NOTCANCELED while // the resolver worker still references the stack-local gaicb. The cancel // path therefore waits (gai_suspend with no timeout) for the worker to // actually finish before letting the stack frame go. The trade-off is that // a wedged DNS server can hold this thread for the system resolver timeout // (~30s by default) past the caller's connection timeout. struct gaicb request {}; struct gaicb *requests[1] = {&request}; struct sigevent sevp {}; struct timespec timeout { timeout_sec, 0 }; request.ar_name = node; request.ar_service = service; request.ar_request = hints; sevp.sigev_notify = SIGEV_NONE; int rc = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp); if (rc != 0) { return rc; } auto cleanup = scope_exit([&] { if (request.ar_result) { freeaddrinfo(request.ar_result); } }); int wait_result = gai_suspend(requests, 1, &timeout); if (wait_result == 0 || wait_result == EAI_ALLDONE) { int gai_result = gai_error(&request); if (gai_result == 0) { *res = request.ar_result; request.ar_result = nullptr; return 0; } return gai_result; } gai_cancel(&request); while (gai_error(&request) == EAI_INPROGRESS) { gai_suspend(requests, 1, nullptr); } return wait_result; #else // Fallback implementation using thread-based timeout for other Unix systems. struct GetAddrInfoState { ~GetAddrInfoState() { if (info) { freeaddrinfo(info); } } std::mutex mutex; std::condition_variable result_cv; bool completed = false; int result = EAI_SYSTEM; std::string node; std::string service; struct addrinfo hints; struct addrinfo *info = nullptr; }; // Allocate on the heap, so the resolver thread can keep using the data. auto state = std::make_shared(); if (node) { state->node = node; } state->service = service; state->hints = *hints; std::thread resolve_thread([state]() { auto thread_result = getaddrinfo(state->node.c_str(), state->service.c_str(), &state->hints, &state->info); std::lock_guard lock(state->mutex); state->result = thread_result; state->completed = true; state->result_cv.notify_one(); }); // Wait for completion or timeout std::unique_lock lock(state->mutex); auto finished = state->result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), [&] { return state->completed; }); if (finished) { // Operation completed within timeout resolve_thread.join(); *res = state->info; state->info = nullptr; // Pass ownership to caller return state->result; } else { // Timeout occurred resolve_thread.detach(); // Let the thread finish in background return EAI_AGAIN; // Return timeout error } #endif #else (void)(timeout_sec); // Unused parameter for non-blocking getaddrinfo return getaddrinfo(node, service, hints, res); #endif } template socket_t create_socket(const std::string &host, const std::string &ip, int port, int address_family, int socket_flags, bool tcp_nodelay, bool ipv6_v6only, SocketOptions socket_options, BindOrConnect bind_or_connect, time_t timeout_sec = 0) { // Get address info const char *node = nullptr; struct addrinfo hints; struct addrinfo *result; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_IP; if (!ip.empty()) { node = ip.c_str(); // Ask getaddrinfo to convert IP in c-string to address hints.ai_family = AF_UNSPEC; hints.ai_flags = AI_NUMERICHOST; } else { if (!host.empty()) { node = host.c_str(); } hints.ai_family = address_family; hints.ai_flags = socket_flags; } #if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H) if (hints.ai_family == AF_UNIX) { const auto addrlen = host.length(); if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; } #ifdef SOCK_CLOEXEC auto sock = socket(hints.ai_family, hints.ai_socktype | SOCK_CLOEXEC, hints.ai_protocol); #else auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol); #endif if (sock != INVALID_SOCKET) { sockaddr_un addr{}; addr.sun_family = AF_UNIX; auto unescaped_host = unescape_abstract_namespace_unix_domain(host); std::copy(unescaped_host.begin(), unescaped_host.end(), addr.sun_path); hints.ai_addr = reinterpret_cast(&addr); hints.ai_addrlen = static_cast( sizeof(addr) - sizeof(addr.sun_path) + addrlen); #ifndef SOCK_CLOEXEC #ifndef _WIN32 fcntl(sock, F_SETFD, FD_CLOEXEC); #endif #endif if (socket_options) { socket_options(sock); } #ifdef _WIN32 // Setting SO_REUSEADDR seems not to work well with AF_UNIX on windows, so // remove the option. set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0); #endif bool dummy; if (!bind_or_connect(sock, hints, dummy)) { close_socket(sock); sock = INVALID_SOCKET; } } return sock; } #endif auto service = std::to_string(port); if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result, timeout_sec)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif return INVALID_SOCKET; } auto se = detail::scope_exit([&] { freeaddrinfo(result); }); for (auto rp = result; rp; rp = rp->ai_next) { // Create a socket #ifdef _WIN32 auto sock = WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED); /** * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1 * and above the socket creation fails on older Windows Systems. * * Let's try to create a socket the old way in this case. * * Reference: * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa * * WSA_FLAG_NO_HANDLE_INHERIT: * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with * SP1, and later * */ if (sock == INVALID_SOCKET) { sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); } #else #ifdef SOCK_CLOEXEC auto sock = socket(rp->ai_family, rp->ai_socktype | SOCK_CLOEXEC, rp->ai_protocol); #else auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); #endif #endif if (sock == INVALID_SOCKET) { continue; } #if !defined _WIN32 && !defined SOCK_CLOEXEC if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { close_socket(sock); continue; } #endif if (tcp_nodelay) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); } if (rp->ai_family == AF_INET6) { set_socket_opt(sock, IPPROTO_IPV6, IPV6_V6ONLY, ipv6_v6only ? 1 : 0); } if (socket_options) { socket_options(sock); } // bind or connect auto quit = false; if (bind_or_connect(sock, *rp, quit)) { return sock; } close_socket(sock); if (quit) { break; } } return INVALID_SOCKET; } void set_nonblocking(socket_t sock, bool nonblocking) { #ifdef _WIN32 auto flags = nonblocking ? 1UL : 0UL; ioctlsocket(sock, FIONBIO, &flags); #else auto flags = fcntl(sock, F_GETFL, 0); fcntl(sock, F_SETFL, nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); #endif } bool is_connection_error() { #ifdef _WIN32 return WSAGetLastError() != WSAEWOULDBLOCK; #else return errno != EINPROGRESS; #endif } bool bind_ip_address(socket_t sock, const std::string &host) { struct addrinfo hints; struct addrinfo *result; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; if (getaddrinfo_with_timeout(host.c_str(), "0", &hints, &result, 0)) { return false; } auto se = detail::scope_exit([&] { freeaddrinfo(result); }); auto ret = false; for (auto rp = result; rp; rp = rp->ai_next) { const auto &ai = *rp; if (!::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { ret = true; break; } } return ret; } #if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__ #define USE_IF2IP #endif #ifdef USE_IF2IP std::string if2ip(int address_family, const std::string &ifn) { struct ifaddrs *ifap; getifaddrs(&ifap); auto se = detail::scope_exit([&] { freeifaddrs(ifap); }); std::string addr_candidate; for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) { if (ifa->ifa_addr && ifn == ifa->ifa_name && (AF_UNSPEC == address_family || ifa->ifa_addr->sa_family == address_family)) { if (ifa->ifa_addr->sa_family == AF_INET) { auto sa = reinterpret_cast(ifa->ifa_addr); char buf[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) { return std::string(buf, INET_ADDRSTRLEN); } } else if (ifa->ifa_addr->sa_family == AF_INET6) { auto sa = reinterpret_cast(ifa->ifa_addr); if (!IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) { char buf[INET6_ADDRSTRLEN] = {}; if (inet_ntop(AF_INET6, &sa->sin6_addr, buf, INET6_ADDRSTRLEN)) { // equivalent to mac's IN6_IS_ADDR_UNIQUE_LOCAL auto s6_addr_head = sa->sin6_addr.s6_addr[0]; if (s6_addr_head == 0xfc || s6_addr_head == 0xfd) { addr_candidate = std::string(buf, INET6_ADDRSTRLEN); } else { return std::string(buf, INET6_ADDRSTRLEN); } } } } } } return addr_candidate; } #endif socket_t create_client_socket( const std::string &host, const std::string &ip, int port, int address_family, bool tcp_nodelay, bool ipv6_v6only, SocketOptions socket_options, time_t connection_timeout_sec, time_t connection_timeout_usec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, const std::string &intf, Error &error) { auto sock = create_socket( host, ip, port, address_family, 0, tcp_nodelay, ipv6_v6only, std::move(socket_options), [&](socket_t sock2, struct addrinfo &ai, bool &quit) -> bool { if (!intf.empty()) { #ifdef USE_IF2IP auto ip_from_if = if2ip(address_family, intf); if (ip_from_if.empty()) { ip_from_if = intf; } if (!bind_ip_address(sock2, ip_from_if)) { error = Error::BindIPAddress; return false; } #endif } set_nonblocking(sock2, true); auto ret = ::connect(sock2, ai.ai_addr, static_cast(ai.ai_addrlen)); if (ret < 0) { if (is_connection_error()) { error = Error::Connection; return false; } error = wait_until_socket_is_ready(sock2, connection_timeout_sec, connection_timeout_usec); if (error != Error::Success) { if (error == Error::ConnectionTimeout) { quit = true; } return false; } } set_nonblocking(sock2, false); set_socket_opt_time(sock2, SOL_SOCKET, SO_RCVTIMEO, read_timeout_sec, read_timeout_usec); set_socket_opt_time(sock2, SOL_SOCKET, SO_SNDTIMEO, write_timeout_sec, write_timeout_usec); error = Error::Success; return true; }, connection_timeout_sec); // Pass DNS timeout if (sock != INVALID_SOCKET) { error = Error::Success; } else { if (error == Error::Success) { error = Error::Connection; } } return sock; } bool get_ip_and_port(const struct sockaddr_storage &addr, socklen_t addr_len, std::string &ip, int &port) { if (addr.ss_family == AF_INET) { port = ntohs(reinterpret_cast(&addr)->sin_port); } else if (addr.ss_family == AF_INET6) { port = ntohs(reinterpret_cast(&addr)->sin6_port); } else { return false; } std::array ipstr{}; if (getnameinfo(reinterpret_cast(&addr), addr_len, ipstr.data(), static_cast(ipstr.size()), nullptr, 0, NI_NUMERICHOST)) { return false; } ip = ipstr.data(); return true; } void get_local_ip_and_port(socket_t sock, std::string &ip, int &port) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); if (!getsockname(sock, reinterpret_cast(&addr), &addr_len)) { get_ip_and_port(addr, addr_len, ip, port); } } void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); if (!getpeername(sock, reinterpret_cast(&addr), &addr_len)) { #ifndef _WIN32 if (addr.ss_family == AF_UNIX) { #if defined(__linux__) struct ucred ucred; socklen_t len = sizeof(ucred); if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == 0) { port = ucred.pid; } #elif defined(SOL_LOCAL) && defined(SO_PEERPID) pid_t pid; socklen_t len = sizeof(pid); if (getsockopt(sock, SOL_LOCAL, SO_PEERPID, &pid, &len) == 0) { port = pid; } #endif return; } #endif get_ip_and_port(addr, addr_len, ip, port); } } // Recursive form retained so operator""_t below can compute hashes for // switch-case labels at compile time (C++11 constexpr forbids loops). Do not // call from runtime paths with arbitrary-length inputs โ€” use str2tag() // instead, which is iterative and stack-safe. constexpr unsigned int str2tag_core(const char *s, size_t l, unsigned int h) { return (l == 0) ? h : str2tag_core( s + 1, l - 1, // Unsets the 6 high bits of h, therefore no overflow happens (((std::numeric_limits::max)() >> 6) & h * 33) ^ static_cast(*s)); } unsigned int str2tag(const std::string &s) { // Iterative form of str2tag_core: the recursive constexpr version is kept // for compile-time UDL evaluation of short string literals, but at runtime // we may receive arbitrarily long inputs (e.g. fuzzed Content-Type) that // would blow the stack with one frame per character. unsigned int h = 0; for (auto c : s) { h = (((std::numeric_limits::max)() >> 6) & h * 33) ^ static_cast(c); } return h; } namespace udl { constexpr unsigned int operator""_t(const char *s, size_t l) { return str2tag_core(s, l, 0); } } // namespace udl std::string find_content_type(const std::string &path, const std::map &user_data, const std::string &default_content_type) { auto ext = file_extension(path); auto it = user_data.find(ext); if (it != user_data.end()) { return it->second; } using udl::operator""_t; switch (str2tag(ext)) { default: return default_content_type; case "css"_t: return "text/css"; case "csv"_t: return "text/csv"; case "htm"_t: case "html"_t: return "text/html"; case "js"_t: case "mjs"_t: return "text/javascript"; case "txt"_t: return "text/plain"; case "vtt"_t: return "text/vtt"; case "apng"_t: return "image/apng"; case "avif"_t: return "image/avif"; case "bmp"_t: return "image/bmp"; case "gif"_t: return "image/gif"; case "png"_t: return "image/png"; case "svg"_t: return "image/svg+xml"; case "webp"_t: return "image/webp"; case "ico"_t: return "image/x-icon"; case "tif"_t: return "image/tiff"; case "tiff"_t: return "image/tiff"; case "jpg"_t: case "jpeg"_t: return "image/jpeg"; case "mp4"_t: return "video/mp4"; case "mpeg"_t: return "video/mpeg"; case "webm"_t: return "video/webm"; case "mp3"_t: return "audio/mp3"; case "mpga"_t: return "audio/mpeg"; case "weba"_t: return "audio/webm"; case "wav"_t: return "audio/wave"; case "otf"_t: return "font/otf"; case "ttf"_t: return "font/ttf"; case "woff"_t: return "font/woff"; case "woff2"_t: return "font/woff2"; case "7z"_t: return "application/x-7z-compressed"; case "atom"_t: return "application/atom+xml"; case "pdf"_t: return "application/pdf"; case "json"_t: return "application/json"; case "rss"_t: return "application/rss+xml"; case "tar"_t: return "application/x-tar"; case "xht"_t: case "xhtml"_t: return "application/xhtml+xml"; case "xslt"_t: return "application/xslt+xml"; case "xml"_t: return "application/xml"; case "gz"_t: return "application/gzip"; case "zip"_t: return "application/zip"; case "wasm"_t: return "application/wasm"; } } std::string extract_media_type(const std::string &content_type, std::map *params = nullptr) { // Extract type/subtype from Content-Type value (RFC 2045) // e.g. "application/json; charset=utf-8" -> "application/json" auto media_type = content_type; auto semicolon_pos = media_type.find(';'); if (semicolon_pos != std::string::npos) { auto param_str = media_type.substr(semicolon_pos + 1); media_type = media_type.substr(0, semicolon_pos); if (params) { // Parse parameters: key=value pairs separated by ';' split(param_str.data(), param_str.data() + param_str.size(), ';', [&](const char *b, const char *e) { std::string key; std::string val; split(b, e, '=', [&](const char *b2, const char *e2) { if (key.empty()) { key.assign(b2, e2); } else { val.assign(b2, e2); } }); if (!key.empty()) { params->emplace(trim_copy(key), trim_double_quotes_copy(val)); } }); } } // Trim whitespace from media type return trim_copy(media_type); } bool can_compress_content_type(const std::string &content_type) { using udl::operator""_t; auto mime_type = extract_media_type(content_type); auto tag = str2tag(mime_type); switch (tag) { case "image/svg+xml"_t: case "application/javascript"_t: case "application/x-javascript"_t: case "application/json"_t: case "application/ld+json"_t: case "application/xml"_t: case "application/xhtml+xml"_t: case "application/rss+xml"_t: case "application/atom+xml"_t: case "application/xslt+xml"_t: case "application/protobuf"_t: return true; case "text/event-stream"_t: return false; default: return !mime_type.rfind("text/", 0); } } bool parse_quality(const char *b, const char *e, std::string &token, double &quality) { quality = 1.0; token.clear(); // Split on first ';': left = token name, right = parameters const char *params_b = nullptr; std::size_t params_len = 0; divide( b, static_cast(e - b), ';', [&](const char *lb, std::size_t llen, const char *rb, std::size_t rlen) { auto r = trim(lb, lb + llen, 0, llen); if (r.first < r.second) { token.assign(lb + r.first, lb + r.second); } params_b = rb; params_len = rlen; }); if (token.empty()) { return false; } if (params_len == 0) { return true; } // Scan parameters for q= (stops on first match) bool invalid = false; split_find(params_b, params_b + params_len, ';', (std::numeric_limits::max)(), [&](const char *pb, const char *pe) -> bool { // Match exactly "q=" or "Q=" (not "query=" etc.) auto len = static_cast(pe - pb); if (len < 2) { return false; } if ((pb[0] != 'q' && pb[0] != 'Q') || pb[1] != '=') { return false; } // Trim the value portion auto r = trim(pb, pe, 2, len); if (r.first >= r.second) { invalid = true; return true; } double v = 0.0; auto res = from_chars(pb + r.first, pb + r.second, v); if (res.ec != std::errc{} || v < 0.0 || v > 1.0) { invalid = true; return true; } quality = v; return true; }); return !invalid; } EncodingType encoding_type(const Request &req, const Response &res) { if (!can_compress_content_type(res.get_header_value("Content-Type"))) { return EncodingType::None; } const auto &s = req.get_header_value("Accept-Encoding"); if (s.empty()) { return EncodingType::None; } // Single-pass: iterate tokens and track the best supported encoding. // Server preference breaks ties (br > gzip > zstd). EncodingType best = EncodingType::None; double best_q = 0.0; // q=0 means "not acceptable" // Server preference: Brotli > Gzip > Zstd (lower = more preferred) auto priority = [](EncodingType t) -> int { switch (t) { case EncodingType::Brotli: return 0; case EncodingType::Gzip: return 1; case EncodingType::Zstd: return 2; default: return 3; } }; std::string name; split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) { double quality = 1.0; if (!parse_quality(b, e, name, quality)) { return; } if (quality <= 0.0) { return; } EncodingType type = EncodingType::None; #ifdef CPPHTTPLIB_BROTLI_SUPPORT if (case_ignore::equal(name, "br")) { type = EncodingType::Brotli; } #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (type == EncodingType::None && case_ignore::equal(name, "gzip")) { type = EncodingType::Gzip; } #endif #ifdef CPPHTTPLIB_ZSTD_SUPPORT if (type == EncodingType::None && case_ignore::equal(name, "zstd")) { type = EncodingType::Zstd; } #endif if (type == EncodingType::None) { return; } // Higher q-value wins; for equal q, server preference breaks ties if (quality > best_q || (quality == best_q && priority(type) < priority(best))) { best_q = quality; best = type; } }); return best; } std::unique_ptr make_compressor(EncodingType type) { #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (type == EncodingType::Gzip) { return detail::make_unique(); } #endif #ifdef CPPHTTPLIB_BROTLI_SUPPORT if (type == EncodingType::Brotli) { return detail::make_unique(); } #endif #ifdef CPPHTTPLIB_ZSTD_SUPPORT if (type == EncodingType::Zstd) { return detail::make_unique(); } #endif (void)type; return nullptr; } const char *encoding_name(EncodingType type) { switch (type) { case EncodingType::Gzip: return "gzip"; case EncodingType::Brotli: return "br"; case EncodingType::Zstd: return "zstd"; default: return ""; } } bool nocompressor::compress(const char *data, size_t data_length, bool /*last*/, Callback callback) { if (!data_length) { return true; } return callback(data, data_length); } #ifdef CPPHTTPLIB_ZLIB_SUPPORT gzip_compressor::gzip_compressor() { std::memset(&strm_, 0, sizeof(strm_)); strm_.zalloc = Z_NULL; strm_.zfree = Z_NULL; strm_.opaque = Z_NULL; is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK; } gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); } bool gzip_compressor::compress(const char *data, size_t data_length, bool last, Callback callback) { assert(is_valid_); do { constexpr size_t max_avail_in = (std::numeric_limits::max)(); strm_.avail_in = static_cast( (std::min)(data_length, max_avail_in)); strm_.next_in = const_cast(reinterpret_cast(data)); data_length -= strm_.avail_in; data += strm_.avail_in; auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH; auto ret = Z_OK; std::array buff{}; do { strm_.avail_out = static_cast(buff.size()); strm_.next_out = reinterpret_cast(buff.data()); ret = deflate(&strm_, flush); if (ret == Z_STREAM_ERROR) { return false; } if (!callback(buff.data(), buff.size() - strm_.avail_out)) { return false; } } while (strm_.avail_out == 0); assert((flush == Z_FINISH && ret == Z_STREAM_END) || (flush == Z_NO_FLUSH && ret == Z_OK)); assert(strm_.avail_in == 0); } while (data_length > 0); return true; } gzip_decompressor::gzip_decompressor() { std::memset(&strm_, 0, sizeof(strm_)); strm_.zalloc = Z_NULL; strm_.zfree = Z_NULL; strm_.opaque = Z_NULL; // 15 is the value of wbits, which should be at the maximum possible value // to ensure that any gzip stream can be decoded. The offset of 32 specifies // that the stream type should be automatically detected either gzip or // deflate. is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK; } gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); } bool gzip_decompressor::is_valid() const { return is_valid_; } bool gzip_decompressor::decompress(const char *data, size_t data_length, Callback callback) { assert(is_valid_); auto ret = Z_OK; do { constexpr size_t max_avail_in = (std::numeric_limits::max)(); strm_.avail_in = static_cast( (std::min)(data_length, max_avail_in)); strm_.next_in = const_cast(reinterpret_cast(data)); data_length -= strm_.avail_in; data += strm_.avail_in; std::array buff{}; while (strm_.avail_in > 0 && ret == Z_OK) { strm_.avail_out = static_cast(buff.size()); strm_.next_out = reinterpret_cast(buff.data()); ret = inflate(&strm_, Z_NO_FLUSH); assert(ret != Z_STREAM_ERROR); switch (ret) { case Z_NEED_DICT: case Z_DATA_ERROR: case Z_MEM_ERROR: inflateEnd(&strm_); return false; } if (!callback(buff.data(), buff.size() - strm_.avail_out)) { return false; } } if (ret != Z_OK && ret != Z_STREAM_END) { return false; } } while (data_length > 0); return true; } #endif #ifdef CPPHTTPLIB_BROTLI_SUPPORT brotli_compressor::brotli_compressor() { state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); } brotli_compressor::~brotli_compressor() { BrotliEncoderDestroyInstance(state_); } bool brotli_compressor::compress(const char *data, size_t data_length, bool last, Callback callback) { std::array buff{}; auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS; auto available_in = data_length; auto next_in = reinterpret_cast(data); for (;;) { if (last) { if (BrotliEncoderIsFinished(state_)) { break; } } else { if (!available_in) { break; } } auto available_out = buff.size(); auto next_out = buff.data(); if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in, &available_out, &next_out, nullptr)) { return false; } auto output_bytes = buff.size() - available_out; if (output_bytes) { callback(reinterpret_cast(buff.data()), output_bytes); } } return true; } brotli_decompressor::brotli_decompressor() { decoder_s = BrotliDecoderCreateInstance(0, 0, 0); decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT : BROTLI_DECODER_RESULT_ERROR; } brotli_decompressor::~brotli_decompressor() { if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); } } bool brotli_decompressor::is_valid() const { return decoder_s; } bool brotli_decompressor::decompress(const char *data, size_t data_length, Callback callback) { if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS || decoder_r == BROTLI_DECODER_RESULT_ERROR) { return 0; } auto next_in = reinterpret_cast(data); size_t avail_in = data_length; size_t total_out; decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT; std::array buff{}; while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { char *next_out = buff.data(); size_t avail_out = buff.size(); decoder_r = BrotliDecoderDecompressStream( decoder_s, &avail_in, &next_in, &avail_out, reinterpret_cast(&next_out), &total_out); if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; } if (!callback(buff.data(), buff.size() - avail_out)) { return false; } } return decoder_r == BROTLI_DECODER_RESULT_SUCCESS || decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; } #endif #ifdef CPPHTTPLIB_ZSTD_SUPPORT zstd_compressor::zstd_compressor() { ctx_ = ZSTD_createCCtx(); ZSTD_CCtx_setParameter(ctx_, ZSTD_c_compressionLevel, ZSTD_fast); } zstd_compressor::~zstd_compressor() { ZSTD_freeCCtx(ctx_); } bool zstd_compressor::compress(const char *data, size_t data_length, bool last, Callback callback) { std::array buff{}; ZSTD_EndDirective mode = last ? ZSTD_e_end : ZSTD_e_continue; ZSTD_inBuffer input = {data, data_length, 0}; bool finished; do { ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0}; size_t const remaining = ZSTD_compressStream2(ctx_, &output, &input, mode); if (ZSTD_isError(remaining)) { return false; } if (!callback(buff.data(), output.pos)) { return false; } finished = last ? (remaining == 0) : (input.pos == input.size); } while (!finished); return true; } zstd_decompressor::zstd_decompressor() { ctx_ = ZSTD_createDCtx(); } zstd_decompressor::~zstd_decompressor() { ZSTD_freeDCtx(ctx_); } bool zstd_decompressor::is_valid() const { return ctx_ != nullptr; } bool zstd_decompressor::decompress(const char *data, size_t data_length, Callback callback) { std::array buff{}; ZSTD_inBuffer input = {data, data_length, 0}; while (input.pos < input.size) { ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0}; size_t const remaining = ZSTD_decompressStream(ctx_, &output, &input); if (ZSTD_isError(remaining)) { return false; } if (!callback(buff.data(), output.pos)) { return false; } } return true; } #endif std::unique_ptr create_decompressor(const std::string &encoding) { std::unique_ptr decompressor; if (encoding == "gzip" || encoding == "deflate") { #ifdef CPPHTTPLIB_ZLIB_SUPPORT decompressor = detail::make_unique(); #endif } else if (encoding.find("br") != std::string::npos) { #ifdef CPPHTTPLIB_BROTLI_SUPPORT decompressor = detail::make_unique(); #endif } else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) { #ifdef CPPHTTPLIB_ZSTD_SUPPORT decompressor = detail::make_unique(); #endif } return decompressor; } // Returns the best available compressor and its Content-Encoding name. // Priority: Brotli > Gzip > Zstd (matches server-side preference). std::pair, const char *> create_compressor() { #ifdef CPPHTTPLIB_BROTLI_SUPPORT return {detail::make_unique(), "br"}; #elif defined(CPPHTTPLIB_ZLIB_SUPPORT) return {detail::make_unique(), "gzip"}; #elif defined(CPPHTTPLIB_ZSTD_SUPPORT) return {detail::make_unique(), "zstd"}; #else return {nullptr, nullptr}; #endif } bool is_prohibited_header_name(const std::string &name) { using udl::operator""_t; switch (str2tag(name)) { case "REMOTE_ADDR"_t: case "REMOTE_PORT"_t: case "LOCAL_ADDR"_t: case "LOCAL_PORT"_t: return true; default: return false; } } bool has_header(const Headers &headers, const std::string &key) { if (is_prohibited_header_name(key)) { return false; } return headers.find(key) != headers.end(); } const char *get_header_value(const Headers &headers, const std::string &key, const char *def, size_t id) { if (is_prohibited_header_name(key)) { #ifndef CPPHTTPLIB_NO_EXCEPTIONS std::string msg = "Prohibited header name '" + key + "' is specified."; throw std::invalid_argument(msg); #else return ""; #endif } auto rng = headers.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second.c_str(); } return def; } size_t get_header_value_count(const Headers &headers, const std::string &key) { auto r = headers.equal_range(key); return static_cast(std::distance(r.first, r.second)); } template typename Map::mapped_type get_multimap_value(const Map &m, const std::string &key, size_t id) { auto rng = m.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second; } return typename Map::mapped_type(); } void set_header(Headers &headers, const std::string &key, const std::string &val) { if (fields::is_field_name(key) && fields::is_field_value(val)) { headers.emplace(key, val); } } bool read_headers(Stream &strm, Headers &headers) { const auto bufsiz = 2048; char buf[bufsiz]; stream_line_reader line_reader(strm, buf, bufsiz); size_t header_count = 0; for (;;) { if (!line_reader.getline()) { return false; } // Check if the line ends with CRLF. auto line_terminator_len = 2; if (line_reader.end_with_crlf()) { // Blank line indicates end of headers. if (line_reader.size() == 2) { break; } } else { #ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR // Blank line indicates end of headers. if (line_reader.size() == 1) { break; } line_terminator_len = 1; #else continue; // Skip invalid line. #endif } if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } // Check header count limit if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } // Exclude line terminator auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; if (!parse_header(line_reader.ptr(), end, [&](const std::string &key, const std::string &val) { headers.emplace(key, val); })) { return false; } header_count++; } // RFC 9110 Section 8.6: Reject requests with multiple Content-Length // headers that have different values to prevent request smuggling. auto cl_range = headers.equal_range("Content-Length"); if (cl_range.first != cl_range.second) { const auto &first_val = cl_range.first->second; for (auto it = std::next(cl_range.first); it != cl_range.second; ++it) { if (it->second != first_val) { return false; } } } return true; } bool read_websocket_upgrade_response(Stream &strm, const std::string &expected_accept, std::string &selected_subprotocol) { // Read status line const auto bufsiz = 2048; char buf[bufsiz]; stream_line_reader line_reader(strm, buf, bufsiz); if (!line_reader.getline()) { 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; } // Parse headers using existing read_headers Headers headers; if (!read_headers(strm, headers)) { 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; } // 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; } // Verify Sec-WebSocket-Accept header value auto it = headers.find("Sec-WebSocket-Accept"); if (it == headers.end() || it->second != expected_accept) { return false; } // Extract negotiated subprotocol auto proto_it = headers.find("Sec-WebSocket-Protocol"); if (proto_it != headers.end()) { selected_subprotocol = proto_it->second; } return true; } enum class ReadContentResult { Success, // Successfully read the content PayloadTooLarge, // The content exceeds the specified payload limit Error // An error occurred while reading the content }; ReadContentResult read_content_with_length( Stream &strm, size_t len, DownloadProgress progress, ContentReceiverWithProgress out, size_t payload_max_length = (std::numeric_limits::max)()) { char buf[CPPHTTPLIB_RECV_BUFSIZ]; detail::BodyReader br; br.stream = &strm; br.has_content_length = true; br.content_length = len; br.payload_max_length = payload_max_length; br.chunked = false; br.bytes_read = 0; br.last_error = Error::Success; size_t r = 0; while (r < len) { auto read_len = static_cast(len - r); auto to_read = (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ); auto n = detail::read_body_content(&strm, br, buf, to_read); if (n <= 0) { // Check if it was a payload size error if (br.last_error == Error::ExceedMaxPayloadSize) { return ReadContentResult::PayloadTooLarge; } return ReadContentResult::Error; } if (!out(buf, static_cast(n), r, len)) { return ReadContentResult::Error; } r += static_cast(n); if (progress) { if (!progress(r, len)) { return ReadContentResult::Error; } } } return ReadContentResult::Success; } ReadContentResult read_content_without_length(Stream &strm, size_t payload_max_length, ContentReceiverWithProgress out) { char buf[CPPHTTPLIB_RECV_BUFSIZ]; size_t r = 0; for (;;) { auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ); if (n == 0) { return ReadContentResult::Success; } if (n < 0) { return ReadContentResult::Error; } // Check if adding this data would exceed the payload limit if (r > payload_max_length || payload_max_length - r < static_cast(n)) { return ReadContentResult::PayloadTooLarge; } if (!out(buf, static_cast(n), r, 0)) { return ReadContentResult::Error; } r += static_cast(n); } return ReadContentResult::Success; } template ReadContentResult read_content_chunked(Stream &strm, T &x, size_t payload_max_length, ContentReceiverWithProgress out) { detail::ChunkedDecoder dec(strm); char buf[CPPHTTPLIB_RECV_BUFSIZ]; size_t total_len = 0; for (;;) { size_t chunk_offset = 0; size_t chunk_total = 0; auto n = dec.read_payload(buf, sizeof(buf), chunk_offset, chunk_total); if (n < 0) { return ReadContentResult::Error; } if (n == 0) { if (!dec.parse_trailers_into(x.trailers, x.headers)) { return ReadContentResult::Error; } return ReadContentResult::Success; } if (total_len > payload_max_length || payload_max_length - total_len < static_cast(n)) { return ReadContentResult::PayloadTooLarge; } if (!out(buf, static_cast(n), chunk_offset, chunk_total)) { return ReadContentResult::Error; } total_len += static_cast(n); } } bool is_chunked_transfer_encoding(const Headers &headers) { return case_ignore::equal( get_header_value(headers, "Transfer-Encoding", "", 0), "chunked"); } template bool prepare_content_receiver(T &x, int &status, ContentReceiverWithProgress receiver, bool decompress, size_t payload_max_length, bool &exceed_payload_max_length, U callback) { if (decompress) { std::string encoding = x.get_header_value("Content-Encoding"); std::unique_ptr decompressor; if (!encoding.empty()) { decompressor = detail::create_decompressor(encoding); if (!decompressor) { // Unsupported encoding or no support compiled in status = StatusCode::UnsupportedMediaType_415; return false; } } if (decompressor) { if (decompressor->is_valid()) { size_t decompressed_size = 0; ContentReceiverWithProgress out = [&](const char *buf, size_t n, size_t off, size_t len) { return decompressor->decompress( buf, n, [&](const char *buf2, size_t n2) { // Guard against zip-bomb: check // decompressed size against limit. if (payload_max_length > 0 && (decompressed_size >= payload_max_length || n2 > payload_max_length - decompressed_size)) { exceed_payload_max_length = true; return false; } decompressed_size += n2; return receiver(buf2, n2, off, len); }); }; return callback(std::move(out)); } else { status = StatusCode::InternalServerError_500; return false; } } } ContentReceiverWithProgress out = [&](const char *buf, size_t n, size_t off, size_t len) { return receiver(buf, n, off, len); }; return callback(std::move(out)); } template bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, DownloadProgress progress, ContentReceiverWithProgress receiver, bool decompress) { bool exceed_payload_max_length = false; return prepare_content_receiver( x, status, std::move(receiver), decompress, payload_max_length, exceed_payload_max_length, [&](const ContentReceiverWithProgress &out) { auto ret = true; // Note: exceed_payload_max_length may also be set by the decompressor // wrapper in prepare_content_receiver when the decompressed payload // size exceeds the limit. if (is_chunked_transfer_encoding(x.headers)) { auto result = read_content_chunked(strm, x, payload_max_length, out); if (result == ReadContentResult::Success) { ret = true; } else if (result == ReadContentResult::PayloadTooLarge) { exceed_payload_max_length = true; ret = false; } else { ret = false; } } else if (!has_header(x.headers, "Content-Length")) { auto result = read_content_without_length(strm, payload_max_length, out); if (result == ReadContentResult::Success) { ret = true; } else if (result == ReadContentResult::PayloadTooLarge) { exceed_payload_max_length = true; ret = false; } else { ret = false; } } else { auto is_invalid_value = false; auto len = get_header_value_u64(x.headers, "Content-Length", (std::numeric_limits::max)(), 0, is_invalid_value); if (is_invalid_value) { ret = false; } else if (len > 0) { auto result = read_content_with_length( strm, len, std::move(progress), out, payload_max_length); ret = (result == ReadContentResult::Success); if (result == ReadContentResult::PayloadTooLarge) { exceed_payload_max_length = true; } } } if (!ret) { status = exceed_payload_max_length ? StatusCode::PayloadTooLarge_413 : StatusCode::BadRequest_400; } return ret; }); } ssize_t write_request_line(Stream &strm, const std::string &method, const std::string &path) { std::string s = method; s += ' '; s += path; s += " HTTP/1.1\r\n"; return strm.write(s.data(), s.size()); } ssize_t write_response_line(Stream &strm, int status) { std::string s = "HTTP/1.1 "; s += std::to_string(status); s += ' '; s += httplib::status_message(status); s += "\r\n"; return strm.write(s.data(), s.size()); } ssize_t write_headers(Stream &strm, const Headers &headers) { ssize_t write_len = 0; for (const auto &x : headers) { std::string s; s = x.first; s += ": "; s += x.second; s += "\r\n"; auto len = strm.write(s.data(), s.size()); if (len < 0) { return len; } write_len += len; } auto len = strm.write("\r\n"); if (len < 0) { return len; } write_len += len; return write_len; } bool write_data(Stream &strm, const char *d, size_t l) { size_t offset = 0; while (offset < l) { auto length = strm.write(d + offset, l - offset); if (length < 0) { return false; } offset += static_cast(length); } return true; } template bool write_content_with_progress(Stream &strm, const ContentProvider &content_provider, size_t offset, size_t length, T is_shutting_down, const UploadProgress &upload_progress, Error &error) { size_t end_offset = offset + length; size_t start_offset = offset; auto ok = true; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { if (ok) { if (write_data(strm, d, l)) { offset += l; if (upload_progress && length > 0) { size_t current_written = offset - start_offset; if (!upload_progress(current_written, length)) { ok = false; return false; } } } else { ok = false; } } return ok; }; data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); }; while (offset < end_offset && !is_shutting_down()) { if (!strm.wait_writable() || !strm.is_peer_alive()) { error = Error::Write; return false; } else if (!content_provider(offset, end_offset - offset, data_sink)) { error = Error::Canceled; return false; } else if (!ok) { error = Error::Write; return false; } } if (offset < end_offset) { // exited due to is_shutting_down(), not completion error = Error::Write; return false; } error = Error::Success; return true; } template bool write_content(Stream &strm, const ContentProvider &content_provider, size_t offset, size_t length, T is_shutting_down, Error &error) { return write_content_with_progress(strm, content_provider, offset, length, is_shutting_down, nullptr, error); } template bool write_content(Stream &strm, const ContentProvider &content_provider, size_t offset, size_t length, const T &is_shutting_down) { auto error = Error::Success; return write_content(strm, content_provider, offset, length, is_shutting_down, error); } template bool write_content_without_length(Stream &strm, const ContentProvider &content_provider, const T &is_shutting_down) { size_t offset = 0; auto data_available = true; auto ok = true; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { if (ok) { offset += l; if (!write_data(strm, d, l)) { ok = false; } } return ok; }; data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); }; data_sink.done = [&](void) { data_available = false; }; while (data_available && !is_shutting_down()) { if (!strm.wait_writable() || !strm.is_peer_alive()) { return false; } else if (!content_provider(offset, 0, data_sink)) { return false; } else if (!ok) { return false; } } return !data_available; // true only if done() was called, false if shutting // down } template bool write_content_chunked(Stream &strm, const ContentProvider &content_provider, const T &is_shutting_down, U &compressor, Error &error) { size_t offset = 0; auto data_available = true; auto ok = true; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { if (ok) { data_available = l > 0; offset += l; std::string payload; if (compressor.compress(d, l, false, [&](const char *data, size_t data_len) { payload.append(data, data_len); return true; })) { if (!payload.empty()) { // Emit chunked response header and footer for each chunk auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; } } } else { ok = false; } } return ok; }; data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); }; auto done_with_trailer = [&](const Headers *trailer) { if (!ok) { return; } data_available = false; std::string payload; if (!compressor.compress(nullptr, 0, true, [&](const char *data, size_t data_len) { payload.append(data, data_len); return true; })) { ok = false; return; } if (!payload.empty()) { // Emit chunked response header and footer for each chunk auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; return; } } constexpr const char done_marker[] = "0\r\n"; if (!write_data(strm, done_marker, str_len(done_marker))) { ok = false; } // Trailer if (trailer) { for (const auto &kv : *trailer) { std::string field_line = kv.first + ": " + kv.second + "\r\n"; if (!write_data(strm, field_line.data(), field_line.size())) { ok = false; } } } constexpr const char crlf[] = "\r\n"; if (!write_data(strm, crlf, str_len(crlf))) { ok = false; } }; data_sink.done = [&](void) { done_with_trailer(nullptr); }; data_sink.done_with_trailer = [&](const Headers &trailer) { done_with_trailer(&trailer); }; while (data_available && !is_shutting_down()) { if (!strm.wait_writable() || !strm.is_peer_alive()) { error = Error::Write; return false; } else if (!content_provider(offset, 0, data_sink)) { error = Error::Canceled; return false; } else if (!ok) { error = Error::Write; return false; } } if (data_available) { // exited due to is_shutting_down(), not done() error = Error::Write; return false; } error = Error::Success; return true; } template bool write_content_chunked(Stream &strm, const ContentProvider &content_provider, const T &is_shutting_down, U &compressor) { auto error = Error::Success; return write_content_chunked(strm, content_provider, is_shutting_down, compressor, error); } template bool redirect(T &cli, Request &req, Response &res, const std::string &path, const std::string &location, Error &error) { Request new_req = req; new_req.path = path; new_req.redirect_count_ -= 1; if (res.status == StatusCode::SeeOther_303 && (req.method != "GET" && req.method != "HEAD")) { new_req.method = "GET"; new_req.body.clear(); new_req.headers.clear(); } Response new_res; auto ret = cli.send(new_req, new_res, error); if (ret) { req = std::move(new_req); res = std::move(new_res); if (res.location.empty()) { res.location = location; } } return ret; } std::string params_to_query_str(const Params ¶ms) { std::string query; for (auto it = params.begin(); it != params.end(); ++it) { if (it != params.begin()) { query += '&'; } query += encode_query_component(it->first); query += '='; query += encode_query_component(it->second); } return query; } void parse_query_text(const char *data, std::size_t size, Params ¶ms) { std::set cache; split(data, data + size, '&', [&](const char *b, const char *e) { std::string kv(b, e); if (cache.find(kv) != cache.end()) { return; } cache.insert(std::move(kv)); std::string key; std::string val; divide(b, static_cast(e - b), '=', [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, std::size_t rhs_size) { key.assign(lhs_data, lhs_size); val.assign(rhs_data, rhs_size); }); if (!key.empty()) { params.emplace(decode_query_component(key), decode_query_component(val)); } }); } void parse_query_text(const std::string &s, Params ¶ms) { parse_query_text(s.data(), s.size(), params); } // Normalize a query string by decoding and re-encoding each key/value pair // while preserving the original parameter order. This avoids double-encoding // and ensures consistent encoding without reordering (unlike Params which // uses std::multimap and sorts keys). std::string normalize_query_string(const std::string &query) { std::string result; split(query.data(), query.data() + query.size(), '&', [&](const char *b, const char *e) { std::string key; std::string val; divide(b, static_cast(e - b), '=', [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, std::size_t rhs_size) { key.assign(lhs_data, lhs_size); val.assign(rhs_data, rhs_size); }); if (!key.empty()) { auto dec_key = decode_query_component(key); auto dec_val = decode_query_component(val); if (!result.empty()) { result += '&'; } result += encode_query_component(dec_key); if (!val.empty() || std::find(b, e, '=') != e) { result += '='; result += encode_query_component(dec_val); } } }); return result; } bool parse_multipart_boundary(const std::string &content_type, std::string &boundary) { std::map params; extract_media_type(content_type, ¶ms); auto it = params.find("boundary"); if (it == params.end()) { return false; } boundary = it->second; return !boundary.empty(); } void parse_disposition_params(const std::string &s, Params ¶ms) { std::set cache; split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) { std::string kv(b, e); if (cache.find(kv) != cache.end()) { return; } cache.insert(kv); std::string key; std::string val; split(b, e, '=', [&](const char *b2, const char *e2) { if (key.empty()) { key.assign(b2, e2); } else { val.assign(b2, e2); } }); if (!key.empty()) { params.emplace(trim_double_quotes_copy((key)), trim_double_quotes_copy((val))); } }); } #ifdef CPPHTTPLIB_NO_EXCEPTIONS bool parse_range_header(const std::string &s, Ranges &ranges) { #else bool parse_range_header(const std::string &s, Ranges &ranges) try { #endif auto is_valid = [](const std::string &str) { return std::all_of(str.cbegin(), str.cend(), [](unsigned char c) { return std::isdigit(c); }); }; if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) { const auto pos = static_cast(6); const auto len = static_cast(s.size() - 6); auto all_valid_ranges = true; split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) { if (!all_valid_ranges) { return; } const auto it = std::find(b, e, '-'); if (it == e) { all_valid_ranges = false; return; } const auto lhs = std::string(b, it); const auto rhs = std::string(it + 1, e); if (!is_valid(lhs) || !is_valid(rhs)) { all_valid_ranges = false; return; } ssize_t first = -1; if (!lhs.empty()) { ssize_t v; auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v); if (res.ec == std::errc{}) { first = v; } } ssize_t last = -1; if (!rhs.empty()) { ssize_t v; auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v); if (res.ec == std::errc{}) { last = v; } } if ((first == -1 && last == -1) || (first != -1 && last != -1 && first > last)) { all_valid_ranges = false; return; } ranges.emplace_back(first, last); }); return all_valid_ranges && !ranges.empty(); } return false; #ifdef CPPHTTPLIB_NO_EXCEPTIONS } #else } catch (...) { return false; } #endif bool parse_accept_header(const std::string &s, std::vector &content_types) { content_types.clear(); // Empty string is considered valid (no preference) if (s.empty()) { return true; } // Check for invalid patterns: leading/trailing commas or consecutive commas if (s.front() == ',' || s.back() == ',' || s.find(",,") != std::string::npos) { return false; } struct AcceptEntry { std::string media_type; double quality; int order; }; std::vector entries; int order = 0; bool has_invalid_entry = false; // Split by comma and parse each entry split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) { std::string entry(b, e); entry = trim_copy(entry); if (entry.empty()) { has_invalid_entry = true; return; } AcceptEntry accept_entry; accept_entry.order = order++; if (!parse_quality(entry.data(), entry.data() + entry.size(), accept_entry.media_type, accept_entry.quality)) { has_invalid_entry = true; return; } // Remove additional parameters from media type accept_entry.media_type = extract_media_type(accept_entry.media_type); // Basic validation of media type format if (accept_entry.media_type.empty()) { has_invalid_entry = true; return; } // Check for basic media type format (should contain '/' or be '*') if (accept_entry.media_type != "*" && accept_entry.media_type.find('/') == std::string::npos) { has_invalid_entry = true; return; } entries.push_back(std::move(accept_entry)); }); // Return false if any invalid entry was found if (has_invalid_entry) { return false; } // Sort by quality (descending), then by original order (ascending) std::sort(entries.begin(), entries.end(), [](const AcceptEntry &a, const AcceptEntry &b) { if (a.quality != b.quality) { return a.quality > b.quality; // Higher quality first } return a.order < b.order; // Earlier order first for same quality }); // Extract sorted media types content_types.reserve(entries.size()); for (auto &entry : entries) { content_types.push_back(std::move(entry.media_type)); } return true; } class FormDataParser { public: FormDataParser() = default; void set_boundary(std::string &&boundary) { boundary_ = std::move(boundary); dash_boundary_crlf_ = dash_ + boundary_ + crlf_; crlf_dash_boundary_ = crlf_ + dash_ + boundary_; } bool is_valid() const { return is_valid_; } bool parse(const char *buf, size_t n, const FormDataHeader &header_callback, const ContentReceiver &content_callback) { buf_append(buf, n); while (buf_size() > 0) { switch (state_) { case 0: { // Initial boundary auto pos = buf_find(dash_boundary_crlf_); if (pos == buf_size()) { return true; } buf_erase(pos + dash_boundary_crlf_.size()); state_ = 1; break; } case 1: { // New entry clear_file_info(); state_ = 2; break; } case 2: { // Headers auto pos = buf_find(crlf_); if (pos > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } while (pos < buf_size()) { // Empty line if (pos == 0) { if (!header_callback(file_)) { is_valid_ = false; return false; } buf_erase(crlf_.size()); state_ = 3; break; } const auto header = buf_head(pos); if (!parse_header(header.data(), header.data() + header.size(), [&](const std::string &, const std::string &) {})) { is_valid_ = false; return false; } // Parse and emplace space trimmed headers into a map if (!parse_header( header.data(), header.data() + header.size(), [&](const std::string &key, const std::string &val) { file_.headers.emplace(key, val); })) { is_valid_ = false; return false; } constexpr const char header_content_type[] = "Content-Type:"; if (start_with_case_ignore(header, header_content_type)) { file_.content_type = trim_copy(header.substr(str_len(header_content_type))); } else { std::string disposition_params; if (parse_content_disposition(header, disposition_params)) { Params params; parse_disposition_params(disposition_params, params); auto it = params.find("name"); if (it != params.end()) { file_.name = it->second; } else { is_valid_ = false; return false; } it = params.find("filename"); if (it != params.end()) { file_.filename = it->second; } it = params.find("filename*"); if (it != params.end()) { // RFC 5987: only UTF-8 encoding is allowed const auto &val = it->second; constexpr const char utf8_prefix[] = "UTF-8''"; constexpr size_t prefix_len = str_len(utf8_prefix); if (val.size() > prefix_len && start_with_case_ignore(val, utf8_prefix)) { file_.filename = decode_path_component( val.substr(prefix_len)); // override... } else { is_valid_ = false; return false; } } } } buf_erase(pos + crlf_.size()); pos = buf_find(crlf_); } if (state_ != 3) { return true; } break; } case 3: { // Body if (crlf_dash_boundary_.size() > buf_size()) { return true; } auto pos = buf_find(crlf_dash_boundary_); if (pos < buf_size()) { if (!content_callback(buf_data(), pos)) { is_valid_ = false; return false; } buf_erase(pos + crlf_dash_boundary_.size()); state_ = 4; } else { auto len = buf_size() - crlf_dash_boundary_.size(); if (len > 0) { if (!content_callback(buf_data(), len)) { is_valid_ = false; return false; } buf_erase(len); } return true; } break; } case 4: { // Boundary if (crlf_.size() > buf_size()) { return true; } if (buf_start_with(crlf_)) { buf_erase(crlf_.size()); state_ = 1; } else { if (dash_.size() > buf_size()) { return true; } if (buf_start_with(dash_)) { buf_erase(dash_.size()); is_valid_ = true; buf_erase(buf_size()); // Remove epilogue } else { return true; } } break; } } } return true; } private: void clear_file_info() { file_.name.clear(); file_.filename.clear(); file_.content_type.clear(); file_.headers.clear(); } bool start_with_case_ignore(const std::string &a, const char *b, size_t offset = 0) const { const auto b_len = strlen(b); if (a.size() < offset + b_len) { return false; } for (size_t i = 0; i < b_len; i++) { if (case_ignore::to_lower(a[offset + i]) != case_ignore::to_lower(b[i])) { return false; } } return true; } // Parses "Content-Disposition: form-data; " without std::regex. // Returns true if header matches, with the params portion in `params_out`. bool parse_content_disposition(const std::string &header, std::string ¶ms_out) const { constexpr const char prefix[] = "Content-Disposition:"; constexpr size_t prefix_len = str_len(prefix); if (!start_with_case_ignore(header, prefix)) { return false; } // Skip whitespace after "Content-Disposition:" auto pos = prefix_len; while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) { pos++; } // Match "form-data;" (case-insensitive) constexpr const char form_data[] = "form-data;"; constexpr size_t form_data_len = str_len(form_data); if (!start_with_case_ignore(header, form_data, pos)) { return false; } pos += form_data_len; // Skip whitespace after "form-data;" while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) { pos++; } params_out = header.substr(pos); return true; } const std::string dash_ = "--"; const std::string crlf_ = "\r\n"; std::string boundary_; std::string dash_boundary_crlf_; std::string crlf_dash_boundary_; size_t state_ = 0; bool is_valid_ = false; FormData file_; // Buffer bool start_with(const std::string &a, size_t spos, size_t epos, const std::string &b) const { if (epos - spos < b.size()) { return false; } for (size_t i = 0; i < b.size(); i++) { if (a[i + spos] != b[i]) { return false; } } return true; } size_t buf_size() const { return buf_epos_ - buf_spos_; } const char *buf_data() const { return &buf_[buf_spos_]; } std::string buf_head(size_t l) const { return buf_.substr(buf_spos_, l); } bool buf_start_with(const std::string &s) const { return start_with(buf_, buf_spos_, buf_epos_, s); } size_t buf_find(const std::string &s) const { auto c = s.front(); size_t off = buf_spos_; while (off < buf_epos_) { auto pos = off; while (true) { if (pos == buf_epos_) { return buf_size(); } if (buf_[pos] == c) { break; } pos++; } auto remaining_size = buf_epos_ - pos; if (s.size() > remaining_size) { return buf_size(); } if (start_with(buf_, pos, buf_epos_, s)) { return pos - buf_spos_; } off = pos + 1; } return buf_size(); } void buf_append(const char *data, size_t n) { auto remaining_size = buf_size(); if (remaining_size > 0 && buf_spos_ > 0) { for (size_t i = 0; i < remaining_size; i++) { buf_[i] = buf_[buf_spos_ + i]; } } buf_spos_ = 0; buf_epos_ = remaining_size; if (remaining_size + n > buf_.size()) { buf_.resize(remaining_size + n); } for (size_t i = 0; i < n; i++) { buf_[buf_epos_ + i] = data[i]; } buf_epos_ += n; } void buf_erase(size_t size) { buf_spos_ += size; } std::string buf_; size_t buf_spos_ = 0; size_t buf_epos_ = 0; }; std::string random_string(size_t length) { constexpr const char data[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; thread_local auto engine([]() { // std::random_device might actually be deterministic on some // platforms, but due to lack of support in the c++ standard library, // doing better requires either some ugly hacks or breaking portability. std::random_device seed_gen; // Request 128 bits of entropy for initialization std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()}; return std::mt19937(seed_sequence); }()); std::string result; for (size_t i = 0; i < length; i++) { result += data[engine() % (sizeof(data) - 1)]; } return result; } std::string make_multipart_data_boundary() { return "--cpp-httplib-multipart-data-" + detail::random_string(16); } bool is_multipart_boundary_chars_valid(const std::string &boundary) { auto valid = true; for (size_t i = 0; i < boundary.size(); i++) { auto c = boundary[i]; if (!std::isalnum(static_cast(c)) && c != '-' && c != '_') { valid = false; break; } } return valid; } template std::string serialize_multipart_formdata_item_begin(const T &item, const std::string &boundary) { std::string body = "--" + boundary + "\r\n"; body += "Content-Disposition: form-data; name=\"" + item.name + "\""; if (!item.filename.empty()) { body += "; filename=\"" + item.filename + "\""; } body += "\r\n"; if (!item.content_type.empty()) { body += "Content-Type: " + item.content_type + "\r\n"; } body += "\r\n"; return body; } std::string serialize_multipart_formdata_item_end() { return "\r\n"; } std::string serialize_multipart_formdata_finish(const std::string &boundary) { return "--" + boundary + "--\r\n"; } std::string serialize_multipart_formdata_get_content_type(const std::string &boundary) { return "multipart/form-data; boundary=" + boundary; } std::string serialize_multipart_formdata(const UploadFormDataItems &items, const std::string &boundary, bool finish = true) { std::string body; for (const auto &item : items) { body += serialize_multipart_formdata_item_begin(item, boundary); body += item.content + serialize_multipart_formdata_item_end(); } if (finish) { body += serialize_multipart_formdata_finish(boundary); } return body; } size_t get_multipart_content_length(const UploadFormDataItems &items, const std::string &boundary) { size_t total = 0; for (const auto &item : items) { total += serialize_multipart_formdata_item_begin(item, boundary).size(); total += item.content.size(); total += serialize_multipart_formdata_item_end().size(); } total += serialize_multipart_formdata_finish(boundary).size(); return total; } struct MultipartSegment { const char *data; size_t size; }; // NOTE: items must outlive the returned ContentProvider // (safe for synchronous use inside Post/Put/Patch) ContentProvider make_multipart_content_provider(const UploadFormDataItems &items, const std::string &boundary) { // Own the per-item header strings and the finish string std::vector owned; owned.reserve(items.size() + 1); for (const auto &item : items) owned.push_back(serialize_multipart_formdata_item_begin(item, boundary)); owned.push_back(serialize_multipart_formdata_finish(boundary)); // Flat segment list: [header, content, "\r\n"] * N + [finish] std::vector segs; segs.reserve(items.size() * 3 + 1); static const char crlf[] = "\r\n"; for (size_t i = 0; i < items.size(); i++) { segs.push_back({owned[i].data(), owned[i].size()}); segs.push_back({items[i].content.data(), items[i].content.size()}); segs.push_back({crlf, 2}); } segs.push_back({owned.back().data(), owned.back().size()}); struct MultipartState { std::vector owned; std::vector segs; std::vector buf = std::vector(CPPHTTPLIB_SEND_BUFSIZ); }; auto state = std::make_shared(); state->owned = std::move(owned); // `segs` holds raw pointers into owned strings; std::string move preserves // the data pointer, so these pointers remain valid after the move above. state->segs = std::move(segs); return [state](size_t offset, size_t length, DataSink &sink) -> bool { // Buffer multiple small segments into fewer, larger writes to avoid // excessive TCP packets when there are many form data items (#2410) auto &buf = state->buf; auto buf_size = buf.size(); size_t buf_len = 0; size_t remaining = length; // Find the first segment containing 'offset' size_t pos = 0; size_t seg_idx = 0; for (; seg_idx < state->segs.size(); seg_idx++) { const auto &seg = state->segs[seg_idx]; if (seg.size > 0 && offset - pos < seg.size) { break; } pos += seg.size; } size_t seg_offset = (seg_idx < state->segs.size()) ? offset - pos : 0; for (; seg_idx < state->segs.size() && remaining > 0; seg_idx++) { const auto &seg = state->segs[seg_idx]; size_t available = seg.size - seg_offset; size_t to_copy = (std::min)(available, remaining); const char *src = seg.data + seg_offset; seg_offset = 0; // only the first segment has a non-zero offset while (to_copy > 0) { size_t space = buf_size - buf_len; size_t chunk = (std::min)(to_copy, space); std::memcpy(buf.data() + buf_len, src, chunk); buf_len += chunk; src += chunk; to_copy -= chunk; remaining -= chunk; if (buf_len == buf_size) { if (!sink.write(buf.data(), buf_len)) { return false; } buf_len = 0; } } } if (buf_len > 0) { return sink.write(buf.data(), buf_len); } return true; }; } void coalesce_ranges(Ranges &ranges, size_t content_length) { if (ranges.size() <= 1) return; // Sort ranges by start position std::sort(ranges.begin(), ranges.end(), [](const Range &a, const Range &b) { return a.first < b.first; }); Ranges coalesced; coalesced.reserve(ranges.size()); for (auto &r : ranges) { auto first_pos = r.first; auto last_pos = r.second; // Handle special cases like in range_error if (first_pos == -1 && last_pos == -1) { first_pos = 0; last_pos = static_cast(content_length); } if (first_pos == -1) { first_pos = static_cast(content_length) - last_pos; last_pos = static_cast(content_length) - 1; } if (last_pos == -1 || last_pos >= static_cast(content_length)) { last_pos = static_cast(content_length) - 1; } // Skip invalid ranges if (!(0 <= first_pos && first_pos <= last_pos && last_pos < static_cast(content_length))) { continue; } // Coalesce with previous range if overlapping or adjacent (but not // identical) if (!coalesced.empty()) { auto &prev = coalesced.back(); // Check if current range overlaps or is adjacent to previous range // but don't coalesce identical ranges (allow duplicates) if (first_pos <= prev.second + 1 && !(first_pos == prev.first && last_pos == prev.second)) { // Extend the previous range prev.second = (std::max)(prev.second, last_pos); continue; } } // Add new range coalesced.emplace_back(first_pos, last_pos); } ranges = std::move(coalesced); } bool range_error(Request &req, Response &res) { if (!req.ranges.empty() && 200 <= res.status && res.status < 300) { if (res.body.empty() && res.content_provider_ && res.content_length_ == 0) { req.ranges.clear(); if (res.status == StatusCode::PartialContent_206) { res.status = StatusCode::OK_200; } return false; } ssize_t content_len = static_cast( res.content_length_ ? res.content_length_ : res.body.size()); std::vector> processed_ranges; size_t overwrapping_count = 0; // NOTE: The following Range check is based on '14.2. Range' in RFC 9110 // 'HTTP Semantics' to avoid potential denial-of-service attacks. // https://www.rfc-editor.org/rfc/rfc9110#section-14.2 // Too many ranges if (req.ranges.size() > CPPHTTPLIB_RANGE_MAX_COUNT) { return true; } for (auto &r : req.ranges) { auto &first_pos = r.first; auto &last_pos = r.second; if (first_pos == -1 && last_pos == -1) { first_pos = 0; last_pos = content_len; } if (first_pos == -1) { first_pos = content_len - last_pos; last_pos = content_len - 1; } // NOTE: RFC-9110 '14.1.2. Byte Ranges': // A client can limit the number of bytes requested without knowing the // size of the selected representation. If the last-pos value is absent, // or if the value is greater than or equal to the current length of the // representation data, the byte range is interpreted as the remainder of // the representation (i.e., the server replaces the value of last-pos // with a value that is one less than the current length of the selected // representation). // https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6 if (last_pos == -1 || last_pos >= content_len) { last_pos = content_len - 1; } // Range must be within content length if (!(0 <= first_pos && first_pos <= last_pos && last_pos <= content_len - 1)) { return true; } // Request must not have more than two overlapping ranges for (const auto &processed_range : processed_ranges) { if (!(last_pos < processed_range.first || first_pos > processed_range.second)) { overwrapping_count++; if (overwrapping_count > 2) { return true; } break; // Only count once per range } } processed_ranges.emplace_back(first_pos, last_pos); } // After validation, coalesce overlapping ranges as per RFC 9110 coalesce_ranges(req.ranges, static_cast(content_len)); } return false; } std::pair get_range_offset_and_length(Range r, size_t content_length) { assert(r.first != -1 && r.second != -1); assert(0 <= r.first && r.first < static_cast(content_length)); assert(r.first <= r.second && r.second < static_cast(content_length)); (void)(content_length); return std::make_pair(static_cast(r.first), static_cast(r.second - r.first) + 1); } std::string make_content_range_header_field( const std::pair &offset_and_length, size_t content_length) { auto st = offset_and_length.first; auto ed = st + offset_and_length.second - 1; std::string field = "bytes "; field += std::to_string(st); field += '-'; field += std::to_string(ed); field += '/'; field += std::to_string(content_length); return field; } template bool process_multipart_ranges_data(const Request &req, const std::string &boundary, const std::string &content_type, size_t content_length, SToken stoken, CToken ctoken, Content content) { for (size_t i = 0; i < req.ranges.size(); i++) { ctoken("--"); stoken(boundary); ctoken("\r\n"); if (!content_type.empty()) { ctoken("Content-Type: "); stoken(content_type); ctoken("\r\n"); } auto offset_and_length = get_range_offset_and_length(req.ranges[i], content_length); ctoken("Content-Range: "); stoken(make_content_range_header_field(offset_and_length, content_length)); ctoken("\r\n"); ctoken("\r\n"); if (!content(offset_and_length.first, offset_and_length.second)) { return false; } ctoken("\r\n"); } ctoken("--"); stoken(boundary); ctoken("--"); return true; } void make_multipart_ranges_data(const Request &req, Response &res, const std::string &boundary, const std::string &content_type, size_t content_length, std::string &data) { process_multipart_ranges_data( req, boundary, content_type, content_length, [&](const std::string &token) { data += token; }, [&](const std::string &token) { data += token; }, [&](size_t offset, size_t length) { assert(offset + length <= content_length); data += res.body.substr(offset, length); return true; }); } size_t get_multipart_ranges_data_length(const Request &req, const std::string &boundary, const std::string &content_type, size_t content_length) { size_t data_length = 0; process_multipart_ranges_data( req, boundary, content_type, content_length, [&](const std::string &token) { data_length += token.size(); }, [&](const std::string &token) { data_length += token.size(); }, [&](size_t /*offset*/, size_t length) { data_length += length; return true; }); return data_length; } template bool write_multipart_ranges_data(Stream &strm, const Request &req, Response &res, const std::string &boundary, const std::string &content_type, size_t content_length, const T &is_shutting_down) { return process_multipart_ranges_data( req, boundary, content_type, content_length, [&](const std::string &token) { strm.write(token); }, [&](const std::string &token) { strm.write(token); }, [&](size_t offset, size_t length) { return write_content(strm, res.content_provider_, offset, length, is_shutting_down); }); } bool has_framed_body(const Request &req) { return is_chunked_transfer_encoding(req.headers) || req.get_header_value_u64("Content-Length") > 0; } bool is_connection_persistent(const Request &req) { auto conn = req.get_header_value("Connection"); if (conn == "close") { return false; } if (req.version == "HTTP/1.0" && conn != "Keep-Alive") { return false; } return true; } bool expect_content(const Request &req) { if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" || req.method == "DELETE") { return true; } return has_framed_body(req); } #ifdef _WIN32 class WSInit { public: WSInit() { WSADATA wsaData; if (WSAStartup(0x0002, &wsaData) == 0) is_valid_ = true; } ~WSInit() { if (is_valid_) WSACleanup(); } bool is_valid_ = false; }; static WSInit wsinit_; #endif bool parse_www_authenticate(const Response &res, std::map &auth, bool is_proxy) { auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; if (res.has_header(auth_key)) { thread_local auto re = std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"); auto s = res.get_header_value(auth_key); auto pos = s.find(' '); if (pos != std::string::npos) { auto type = s.substr(0, pos); if (type == "Basic") { return false; } else if (type == "Digest") { s = s.substr(pos + 1); auto beg = std::sregex_iterator(s.begin(), s.end(), re); for (auto i = beg; i != std::sregex_iterator(); ++i) { const auto &m = *i; auto key = s.substr(static_cast(m.position(1)), static_cast(m.length(1))); auto val = m.length(2) > 0 ? s.substr(static_cast(m.position(2)), static_cast(m.length(2))) : s.substr(static_cast(m.position(3)), static_cast(m.length(3))); auth[std::move(key)] = std::move(val); } return true; } } } return false; } class ContentProviderAdapter { public: explicit ContentProviderAdapter( ContentProviderWithoutLength &&content_provider) : content_provider_(std::move(content_provider)) {} bool operator()(size_t offset, size_t, DataSink &sink) { return content_provider_(offset, sink); } private: ContentProviderWithoutLength content_provider_; }; // NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5 namespace fields { bool is_token_char(char c) { return std::isalnum(static_cast(c)) || c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; } bool is_token(const std::string &s) { if (s.empty()) { return false; } for (auto c : s) { if (!is_token_char(c)) { return false; } } return true; } bool is_field_name(const std::string &s) { return is_token(s); } bool is_vchar(char c) { return c >= 33 && c <= 126; } bool is_obs_text(char c) { return 128 <= static_cast(c); } bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); } bool is_field_content(const std::string &s) { if (s.empty()) { return true; } if (s.size() == 1) { return is_field_vchar(s[0]); } else if (s.size() == 2) { return is_field_vchar(s[0]) && is_field_vchar(s[1]); } else { size_t i = 0; if (!is_field_vchar(s[i])) { return false; } i++; while (i < s.size() - 1) { auto c = s[i++]; if (c == ' ' || c == '\t' || is_field_vchar(c)) { } else { return false; } } return is_field_vchar(s[i]); } } bool is_field_value(const std::string &s) { return is_field_content(s); } } // namespace fields bool perform_websocket_handshake(Stream &strm, const std::string &host, int port, const std::string &path, const Headers &headers, std::string &selected_subprotocol) { // Validate path and host if (!fields::is_field_value(path) || !fields::is_field_value(host)) { return false; } // Validate user-provided headers for (const auto &h : headers) { if (!fields::is_field_name(h.first) || !fields::is_field_value(h.second)) { return false; } } // Generate random Sec-WebSocket-Key thread_local std::mt19937 rng(std::random_device{}()); std::string key_bytes(16, '\0'); for (size_t i = 0; i < 16; i += 4) { auto r = rng(); std::memcpy(&key_bytes[i], &r, (std::min)(size_t(4), size_t(16 - i))); } auto client_key = base64_encode(key_bytes); // Build upgrade request std::string req_str = "GET " + path + " HTTP/1.1\r\n"; req_str += "Host: " + host + ":" + std::to_string(port) + "\r\n"; req_str += "Upgrade: websocket\r\n"; req_str += "Connection: Upgrade\r\n"; req_str += "Sec-WebSocket-Key: " + client_key + "\r\n"; req_str += "Sec-WebSocket-Version: 13\r\n"; for (const auto &h : headers) { req_str += h.first + ": " + h.second + "\r\n"; } req_str += "\r\n"; if (strm.write(req_str.data(), req_str.size()) < 0) { 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); } } // namespace detail /* * Group 2: detail namespace - SSL common utilities */ #ifdef CPPHTTPLIB_SSL_ENABLED namespace detail { class SSLSocketStream final : public Stream { public: SSLSocketStream( socket_t sock, tls::session_t session, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec = 0, std::chrono::time_point start_time = (std::chrono::steady_clock::time_point::min)()); ~SSLSocketStream() override; bool is_readable() const override; bool wait_readable() const override; bool wait_writable() const override; bool is_peer_alive() const override; ssize_t read(char *ptr, size_t size) override; ssize_t write(const char *ptr, size_t size) override; void get_remote_ip_and_port(std::string &ip, int &port) const override; void get_local_ip_and_port(std::string &ip, int &port) const override; socket_t socket() const override; time_t duration() const override; void set_read_timeout(time_t sec, time_t usec = 0) override; private: socket_t sock_; tls::session_t session_; time_t read_timeout_sec_; time_t read_timeout_usec_; time_t write_timeout_sec_; time_t write_timeout_usec_; time_t max_timeout_msec_; const std::chrono::time_point start_time_; }; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT std::string message_digest(const std::string &s, const EVP_MD *algo) { auto context = std::unique_ptr( EVP_MD_CTX_new(), EVP_MD_CTX_free); unsigned int hash_length = 0; unsigned char hash[EVP_MAX_MD_SIZE]; EVP_DigestInit_ex(context.get(), algo, nullptr); EVP_DigestUpdate(context.get(), s.c_str(), s.size()); EVP_DigestFinal_ex(context.get(), hash, &hash_length); std::stringstream ss; for (auto i = 0u; i < hash_length; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(hash[i]); } return ss.str(); } std::string MD5(const std::string &s) { return message_digest(s, EVP_md5()); } std::string SHA_256(const std::string &s) { return message_digest(s, EVP_sha256()); } std::string SHA_512(const std::string &s) { return message_digest(s, EVP_sha512()); } #elif defined(CPPHTTPLIB_MBEDTLS_SUPPORT) namespace { template std::string hash_to_hex(const unsigned char (&hash)[N]) { std::stringstream ss; for (size_t i = 0; i < N; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(hash[i]); } return ss.str(); } } // namespace std::string MD5(const std::string &s) { unsigned char hash[16]; #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_md5(reinterpret_cast(s.c_str()), s.size(), hash); #else mbedtls_md5_ret(reinterpret_cast(s.c_str()), s.size(), hash); #endif return hash_to_hex(hash); } std::string SHA_256(const std::string &s) { unsigned char hash[32]; #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_sha256(reinterpret_cast(s.c_str()), s.size(), hash, 0); #else mbedtls_sha256_ret(reinterpret_cast(s.c_str()), s.size(), hash, 0); #endif return hash_to_hex(hash); } std::string SHA_512(const std::string &s) { unsigned char hash[64]; #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_sha512(reinterpret_cast(s.c_str()), s.size(), hash, 0); #else mbedtls_sha512_ret(reinterpret_cast(s.c_str()), s.size(), hash, 0); #endif return hash_to_hex(hash); } #elif defined(CPPHTTPLIB_WOLFSSL_SUPPORT) namespace { template std::string hash_to_hex(const unsigned char (&hash)[N]) { std::stringstream ss; for (size_t i = 0; i < N; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(hash[i]); } return ss.str(); } } // namespace std::string MD5(const std::string &s) { unsigned char hash[WC_MD5_DIGEST_SIZE]; wc_Md5Hash(reinterpret_cast(s.c_str()), static_cast(s.size()), hash); return hash_to_hex(hash); } std::string SHA_256(const std::string &s) { unsigned char hash[WC_SHA256_DIGEST_SIZE]; wc_Sha256Hash(reinterpret_cast(s.c_str()), static_cast(s.size()), hash); return hash_to_hex(hash); } std::string SHA_512(const std::string &s) { unsigned char hash[WC_SHA512_DIGEST_SIZE]; wc_Sha512Hash(reinterpret_cast(s.c_str()), static_cast(s.size()), hash); return hash_to_hex(hash); } #endif bool is_ip_address(const std::string &host) { struct in_addr addr4; struct in6_addr addr6; return inet_pton(AF_INET, host.c_str(), &addr4) == 1 || inet_pton(AF_INET6, host.c_str(), &addr6) == 1; } template bool process_server_socket_ssl( const std::atomic &svr_sock, tls::session_t session, socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, T callback) { return process_server_socket_core( svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, [&](bool close_connection, bool &connection_closed) { SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); return callback(strm, close_connection, connection_closed); }); } template bool process_client_socket_ssl( tls::session_t session, socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, std::chrono::time_point start_time, T callback) { SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec, max_timeout_msec, start_time); return callback(strm); } std::pair make_digest_authentication_header( const Request &req, const std::map &auth, size_t cnonce_count, const std::string &cnonce, const std::string &username, const std::string &password, bool is_proxy = false) { std::string nc; { std::stringstream ss; ss << std::setfill('0') << std::setw(8) << std::hex << cnonce_count; nc = ss.str(); } std::string qop; if (auth.find("qop") != auth.end()) { qop = auth.at("qop"); if (qop.find("auth-int") != std::string::npos) { qop = "auth-int"; } else if (qop.find("auth") != std::string::npos) { qop = "auth"; } else { qop.clear(); } } std::string algo = "MD5"; if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); } std::string response; { auto H = algo == "SHA-256" ? detail::SHA_256 : algo == "SHA-512" ? detail::SHA_512 : detail::MD5; auto A1 = username + ":" + auth.at("realm") + ":" + password; auto A2 = req.method + ":" + req.path; if (qop == "auth-int") { A2 += ":" + H(req.body); } if (qop.empty()) { response = H(H(A1) + ":" + auth.at("nonce") + ":" + H(A2)); } else { response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce + ":" + qop + ":" + H(A2)); } } auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("opaque") : ""; auto field = "Digest username=\"" + username + "\", realm=\"" + auth.at("realm") + "\", nonce=\"" + auth.at("nonce") + "\", uri=\"" + req.path + "\", algorithm=" + algo + (qop.empty() ? ", response=\"" : ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce + "\", response=\"") + response + "\"" + (opaque.empty() ? "" : ", opaque=\"" + opaque + "\""); auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; return std::make_pair(key, field); } bool match_hostname(const std::string &pattern, const std::string &hostname) { // Exact match (case-insensitive) if (detail::case_ignore::equal(hostname, pattern)) { return true; } // Split both pattern and hostname into components by '.' std::vector pattern_components; if (!pattern.empty()) { split(pattern.data(), pattern.data() + pattern.size(), '.', [&](const char *b, const char *e) { pattern_components.emplace_back(b, e); }); } std::vector host_components; if (!hostname.empty()) { split(hostname.data(), hostname.data() + hostname.size(), '.', [&](const char *b, const char *e) { host_components.emplace_back(b, e); }); } // Component count must match if (host_components.size() != pattern_components.size()) { return false; } // Compare each component with wildcard support // Supports: "*" (full wildcard), "prefix*" (partial wildcard) // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484 auto itr = pattern_components.begin(); for (const auto &h : host_components) { auto &p = *itr; if (!detail::case_ignore::equal(p, h) && p != "*") { bool partial_match = false; if (!p.empty() && p[p.size() - 1] == '*') { const auto prefix_length = p.size() - 1; if (prefix_length == 0) { partial_match = true; } else if (h.size() >= prefix_length) { partial_match = std::equal(p.begin(), p.begin() + static_cast( prefix_length), h.begin(), [](const char ca, const char cb) { return detail::case_ignore::to_lower(ca) == detail::case_ignore::to_lower(cb); }); } } if (!partial_match) { return false; } } ++itr; } return true; } #ifdef _WIN32 // Verify certificate using Windows CertGetCertificateChain API. // This provides real-time certificate validation with Windows Update // integration, independent of the TLS backend (OpenSSL or MbedTLS). bool verify_cert_with_windows_schannel(const std::vector &der_cert, const std::string &hostname, bool verify_hostname, uint64_t &out_error) { if (der_cert.empty()) { return false; } out_error = 0; // Create Windows certificate context from DER data auto cert_context = CertCreateCertificateContext( X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(), static_cast(der_cert.size())); if (!cert_context) { out_error = GetLastError(); return false; } auto cert_guard = scope_exit([&] { CertFreeCertificateContext(cert_context); }); // Setup chain parameters CERT_CHAIN_PARA chain_para = {}; chain_para.cbSize = sizeof(chain_para); // Build certificate chain with revocation checking PCCERT_CHAIN_CONTEXT chain_context = nullptr; auto chain_result = CertGetCertificateChain( nullptr, cert_context, nullptr, cert_context->hCertStore, &chain_para, CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT | CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT, nullptr, &chain_context); if (!chain_result || !chain_context) { out_error = GetLastError(); return false; } auto chain_guard = scope_exit([&] { CertFreeCertificateChain(chain_context); }); // Check if chain has errors if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) { out_error = chain_context->TrustStatus.dwErrorStatus; return false; } // Verify SSL policy SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {}; extra_policy_para.cbSize = sizeof(extra_policy_para); #ifdef AUTHTYPE_SERVER extra_policy_para.dwAuthType = AUTHTYPE_SERVER; #endif std::wstring whost; if (verify_hostname) { whost = u8string_to_wstring(hostname.c_str()); extra_policy_para.pwszServerName = const_cast(whost.c_str()); } CERT_CHAIN_POLICY_PARA policy_para = {}; policy_para.cbSize = sizeof(policy_para); #ifdef CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS policy_para.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS; #else policy_para.dwFlags = 0; #endif policy_para.pvExtraPolicyPara = &extra_policy_para; CERT_CHAIN_POLICY_STATUS policy_status = {}; policy_status.cbSize = sizeof(policy_status); if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chain_context, &policy_para, &policy_status)) { out_error = GetLastError(); return false; } if (policy_status.dwError != 0) { out_error = policy_status.dwError; return false; } return true; } #endif // _WIN32 // Loads CA file/dir configuration and applies the system CA policy to a // client TLS context. PEM data and native stores are applied to the context // directly at set time; has_custom_store reflects them for the Auto policy // decision. bool load_client_ca_config(tls::ctx_t ctx, const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path, bool has_custom_store, SystemCAMode mode, uint64_t &backend_error) { auto ret = true; if (!ca_cert_file_path.empty()) { if (!tls::load_ca_file(ctx, ca_cert_file_path.c_str())) { backend_error = tls::get_error(); ret = false; } } else if (!ca_cert_dir_path.empty()) { if (!tls::load_ca_dir(ctx, ca_cert_dir_path.c_str())) { backend_error = tls::get_error(); ret = false; } } auto has_custom_ca = !ca_cert_file_path.empty() || !ca_cert_dir_path.empty() || has_custom_store; if (mode == SystemCAMode::Enabled || (mode == SystemCAMode::Auto && !has_custom_ca)) { if (!tls::load_system_certs(ctx)) { backend_error = tls::get_error(); } } 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) { using namespace tls; if (!ctx) { return false; } bool is_ip = is_ip_address(host); #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() set_verify_client(ctx, server_certificate_verification); #endif session = create_session(ctx, sock); if (!session) { return false; } // 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()); } } if (!connect_nonblocking(session, sock, timeout_sec, timeout_usec, nullptr)) { return false; } if (server_certificate_verification) { if (get_verify_result(session) != 0) { return false; } // 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; } auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); }); if (!verify_hostname(server_cert, host.c_str())) { return false; } } return true; } } // namespace detail #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 3: httplib namespace - Non-SSL public API implementations */ void default_socket_options(socket_t sock) { set_socket_opt(sock, SOL_SOCKET, #ifdef SO_REUSEPORT SO_REUSEPORT, #else SO_REUSEADDR, #endif 1); } bool set_socket_opt(socket_t sock, int level, int optname, int optval) { return detail::set_socket_opt_impl(sock, level, optname, &optval, sizeof(optval)); } std::string get_bearer_token_auth(const Request &req) { if (req.has_header("Authorization")) { constexpr auto bearer_header_prefix_len = detail::str_len("Bearer "); return req.get_header_value("Authorization") .substr(bearer_header_prefix_len); } return ""; } const char *status_message(int status) { switch (status) { case StatusCode::Continue_100: return "Continue"; case StatusCode::SwitchingProtocol_101: return "Switching Protocol"; case StatusCode::Processing_102: return "Processing"; case StatusCode::EarlyHints_103: return "Early Hints"; case StatusCode::OK_200: return "OK"; case StatusCode::Created_201: return "Created"; case StatusCode::Accepted_202: return "Accepted"; case StatusCode::NonAuthoritativeInformation_203: return "Non-Authoritative Information"; case StatusCode::NoContent_204: return "No Content"; case StatusCode::ResetContent_205: return "Reset Content"; case StatusCode::PartialContent_206: return "Partial Content"; case StatusCode::MultiStatus_207: return "Multi-Status"; case StatusCode::AlreadyReported_208: return "Already Reported"; case StatusCode::IMUsed_226: return "IM Used"; case StatusCode::MultipleChoices_300: return "Multiple Choices"; case StatusCode::MovedPermanently_301: return "Moved Permanently"; case StatusCode::Found_302: return "Found"; case StatusCode::SeeOther_303: return "See Other"; case StatusCode::NotModified_304: return "Not Modified"; case StatusCode::UseProxy_305: return "Use Proxy"; case StatusCode::unused_306: return "unused"; case StatusCode::TemporaryRedirect_307: return "Temporary Redirect"; case StatusCode::PermanentRedirect_308: return "Permanent Redirect"; case StatusCode::BadRequest_400: return "Bad Request"; case StatusCode::Unauthorized_401: return "Unauthorized"; case StatusCode::PaymentRequired_402: return "Payment Required"; case StatusCode::Forbidden_403: return "Forbidden"; case StatusCode::NotFound_404: return "Not Found"; case StatusCode::MethodNotAllowed_405: return "Method Not Allowed"; case StatusCode::NotAcceptable_406: return "Not Acceptable"; case StatusCode::ProxyAuthenticationRequired_407: return "Proxy Authentication Required"; case StatusCode::RequestTimeout_408: return "Request Timeout"; case StatusCode::Conflict_409: return "Conflict"; case StatusCode::Gone_410: return "Gone"; case StatusCode::LengthRequired_411: return "Length Required"; case StatusCode::PreconditionFailed_412: return "Precondition Failed"; case StatusCode::PayloadTooLarge_413: return "Payload Too Large"; case StatusCode::UriTooLong_414: return "URI Too Long"; case StatusCode::UnsupportedMediaType_415: return "Unsupported Media Type"; case StatusCode::RangeNotSatisfiable_416: return "Range Not Satisfiable"; case StatusCode::ExpectationFailed_417: return "Expectation Failed"; case StatusCode::ImATeapot_418: return "I'm a teapot"; case StatusCode::MisdirectedRequest_421: return "Misdirected Request"; case StatusCode::UnprocessableContent_422: return "Unprocessable Content"; case StatusCode::Locked_423: return "Locked"; case StatusCode::FailedDependency_424: return "Failed Dependency"; case StatusCode::TooEarly_425: return "Too Early"; case StatusCode::UpgradeRequired_426: return "Upgrade Required"; case StatusCode::PreconditionRequired_428: return "Precondition Required"; case StatusCode::TooManyRequests_429: return "Too Many Requests"; case StatusCode::RequestHeaderFieldsTooLarge_431: return "Request Header Fields Too Large"; case StatusCode::UnavailableForLegalReasons_451: return "Unavailable For Legal Reasons"; case StatusCode::NotImplemented_501: return "Not Implemented"; case StatusCode::BadGateway_502: return "Bad Gateway"; case StatusCode::ServiceUnavailable_503: return "Service Unavailable"; case StatusCode::GatewayTimeout_504: return "Gateway Timeout"; case StatusCode::HttpVersionNotSupported_505: return "HTTP Version Not Supported"; case StatusCode::VariantAlsoNegotiates_506: return "Variant Also Negotiates"; case StatusCode::InsufficientStorage_507: return "Insufficient Storage"; case StatusCode::LoopDetected_508: return "Loop Detected"; case StatusCode::NotExtended_510: return "Not Extended"; case StatusCode::NetworkAuthenticationRequired_511: return "Network Authentication Required"; default: case StatusCode::InternalServerError_500: return "Internal Server Error"; } } std::string to_string(const Error error) { switch (error) { case Error::Success: return "Success (no error)"; case Error::Unknown: return "Unknown"; case Error::Connection: return "Could not establish connection"; case Error::BindIPAddress: return "Failed to bind IP address"; case Error::Read: return "Failed to read connection"; case Error::Write: return "Failed to write connection"; case Error::ExceedRedirectCount: return "Maximum redirect count exceeded"; case Error::Canceled: return "Connection handling canceled"; case Error::SSLConnection: return "SSL connection failed"; case Error::SSLLoadingCerts: return "SSL certificate loading failed"; case Error::SSLServerVerification: return "SSL server verification failed"; case Error::SSLServerHostnameVerification: return "SSL server hostname verification failed"; case Error::UnsupportedMultipartBoundaryChars: return "Unsupported HTTP multipart boundary characters"; case Error::Compression: return "Compression failed"; case Error::ConnectionTimeout: return "Connection timed out"; case Error::ProxyConnection: return "Proxy connection failed"; case Error::ConnectionClosed: return "Connection closed by server"; case Error::Timeout: return "Read timeout"; case Error::ResourceExhaustion: return "Resource exhaustion"; case Error::TooManyFormDataFiles: return "Too many form data files"; case Error::ExceedMaxPayloadSize: return "Exceeded maximum payload size"; case Error::ExceedUriMaxLength: return "Exceeded maximum URI length"; case Error::ExceedMaxSocketDescriptorCount: return "Exceeded maximum socket descriptor count"; case Error::InvalidRequestLine: return "Invalid request line"; case Error::InvalidHTTPMethod: return "Invalid HTTP method"; case Error::InvalidHTTPVersion: return "Invalid HTTP version"; case Error::InvalidHeaders: return "Invalid headers"; case Error::MultipartParsing: return "Multipart parsing failed"; case Error::OpenFile: return "Failed to open file"; case Error::Listen: return "Failed to listen on socket"; case Error::GetSockName: return "Failed to get socket name"; case Error::UnsupportedAddressFamily: return "Unsupported address family"; case Error::HTTPParsing: return "HTTP parsing failed"; case Error::InvalidRangeHeader: return "Invalid Range header"; default: break; } return "Invalid"; } std::ostream &operator<<(std::ostream &os, const Error &obj) { os << to_string(obj); os << " (" << static_cast::type>(obj) << ')'; return os; } std::string hosted_at(const std::string &hostname) { std::vector addrs; hosted_at(hostname, addrs); if (addrs.empty()) { return std::string(); } return addrs[0]; } void hosted_at(const std::string &hostname, std::vector &addrs) { struct addrinfo hints; struct addrinfo *result; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; if (detail::getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints, &result, 0)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif return; } auto se = detail::scope_exit([&] { freeaddrinfo(result); }); for (auto rp = result; rp; rp = rp->ai_next) { const auto &addr = *reinterpret_cast(rp->ai_addr); std::string ip; auto dummy = -1; if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage), ip, dummy)) { addrs.emplace_back(std::move(ip)); } } } std::string encode_uri_component(const std::string &value) { std::ostringstream escaped; escaped.fill('0'); escaped << std::hex; for (auto c : value) { if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')') { escaped << c; } else { escaped << std::uppercase; escaped << '%' << std::setw(2) << static_cast(static_cast(c)); escaped << std::nouppercase; } } return escaped.str(); } std::string encode_uri(const std::string &value) { std::ostringstream escaped; escaped.fill('0'); escaped << std::hex; for (auto c : value) { if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')' || c == ';' || c == '/' || c == '?' || c == ':' || c == '@' || c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#') { escaped << c; } else { escaped << std::uppercase; escaped << '%' << std::setw(2) << static_cast(static_cast(c)); escaped << std::nouppercase; } } return escaped.str(); } std::string decode_uri_component(const std::string &value) { std::string result; for (size_t i = 0; i < value.size(); i++) { if (value[i] == '%' && i + 2 < value.size()) { auto val = 0; if (detail::from_hex_to_i(value, i + 1, 2, val)) { result += static_cast(val); i += 2; } else { result += value[i]; } } else { result += value[i]; } } return result; } std::string decode_uri(const std::string &value) { std::string result; for (size_t i = 0; i < value.size(); i++) { if (value[i] == '%' && i + 2 < value.size()) { auto val = 0; if (detail::from_hex_to_i(value, i + 1, 2, val)) { result += static_cast(val); i += 2; } else { result += value[i]; } } else { result += value[i]; } } return result; } std::string encode_path_component(const std::string &component) { std::string result; result.reserve(component.size() * 3); for (size_t i = 0; i < component.size(); i++) { auto c = static_cast(component[i]); // Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~" if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') { result += static_cast(c); } // Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / // "," / ";" / "=" else if (c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || c == '=') { result += static_cast(c); } // Colon is allowed in path segments except first segment else if (c == ':') { result += static_cast(c); } // @ is allowed in path else if (c == '@') { result += static_cast(c); } else { result += '%'; char hex[3]; snprintf(hex, sizeof(hex), "%02X", c); result.append(hex, 2); } } return result; } std::string decode_path_component(const std::string &component) { std::string result; result.reserve(component.size()); for (size_t i = 0; i < component.size(); i++) { if (component[i] == '%' && i + 1 < component.size()) { if (component[i + 1] == 'u') { // Unicode %uXXXX encoding auto val = 0; if (detail::from_hex_to_i(component, i + 2, 4, val)) { // 4 digits Unicode codes: val is 0x0000-0xFFFF (from 4 hex digits), // so to_utf8 writes at most 3 bytes. buff[4] is safe. char buff[4]; size_t len = detail::to_utf8(val, buff); if (len > 0) { result.append(buff, len); } i += 5; // 'u0000' } else { result += component[i]; } } else { // Standard %XX encoding auto val = 0; if (detail::from_hex_to_i(component, i + 1, 2, val)) { // 2 digits hex codes result += static_cast(val); i += 2; // 'XX' } else { result += component[i]; } } } else { result += component[i]; } } return result; } std::string encode_query_component(const std::string &component, bool space_as_plus) { std::string result; result.reserve(component.size() * 3); for (size_t i = 0; i < component.size(); i++) { auto c = static_cast(component[i]); // Unreserved characters per RFC 3986 if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') { result += static_cast(c); } // Space handling else if (c == ' ') { if (space_as_plus) { result += '+'; } else { result += "%20"; } } // Plus sign handling else if (c == '+') { if (space_as_plus) { result += "%2B"; } else { result += static_cast(c); } } // Query-safe sub-delimiters (excluding & and = which are query delimiters) else if (c == '!' || c == '$' || c == '\'' || c == '(' || c == ')' || c == '*' || c == ',' || c == ';') { result += static_cast(c); } // Colon and @ are allowed in query else if (c == ':' || c == '@') { result += static_cast(c); } // Forward slash is allowed in query values else if (c == '/') { result += static_cast(c); } // Question mark is allowed in query values (after first ?) else if (c == '?') { result += static_cast(c); } else { result += '%'; char hex[3]; snprintf(hex, sizeof(hex), "%02X", c); result.append(hex, 2); } } return result; } std::string decode_query_component(const std::string &component, bool plus_as_space) { std::string result; result.reserve(component.size()); for (size_t i = 0; i < component.size(); i++) { if (component[i] == '%' && i + 2 < component.size()) { auto val = 0; if (detail::from_hex_to_i(component, i + 1, 2, val)) { result += static_cast(val); i += 2; } else { result += component[i]; } } else if (component[i] == '+' && plus_as_space) { result += ' '; // + becomes space in form-urlencoded } else { result += component[i]; } } return result; } std::string sanitize_filename(const std::string &filename) { // Extract basename: find the last path separator (/ or \) auto pos = filename.find_last_of("/\\"); auto result = (pos != std::string::npos) ? filename.substr(pos + 1) : filename; // Strip null bytes result.erase(std::remove(result.begin(), result.end(), '\0'), result.end()); // Trim whitespace { auto start = result.find_first_not_of(" \t"); auto end = result.find_last_not_of(" \t"); result = (start == std::string::npos) ? "" : result.substr(start, end - start + 1); } // Reject . and .. if (result == "." || result == "..") { return ""; } return result; } std::string append_query_params(const std::string &path, const Params ¶ms) { std::string path_with_query = path; thread_local const std::regex re("[^?]+\\?.*"); auto delm = std::regex_match(path, re) ? '&' : '?'; path_with_query += delm + detail::params_to_query_str(params); return path_with_query; } // Header utilities std::pair make_range_header(const Ranges &ranges) { std::string field = "bytes="; auto i = 0; for (const auto &r : ranges) { if (i != 0) { field += ", "; } if (r.first != -1) { field += std::to_string(r.first); } field += '-'; if (r.second != -1) { field += std::to_string(r.second); } i++; } return std::make_pair("Range", std::move(field)); } std::pair make_basic_authentication_header(const std::string &username, const std::string &password, bool is_proxy) { auto field = "Basic " + detail::base64_encode(username + ":" + password); auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; return std::make_pair(key, std::move(field)); } std::pair make_bearer_token_authentication_header(const std::string &token, bool is_proxy = false) { auto field = "Bearer " + token; auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; return std::make_pair(key, std::move(field)); } // Request implementation size_t Request::get_header_value_u64(const std::string &key, size_t def, size_t id) const { return detail::get_header_value_u64(headers, key, def, id); } bool Request::has_header(const std::string &key) const { return detail::has_header(headers, key); } std::string Request::get_header_value(const std::string &key, const char *def, size_t id) const { return detail::get_header_value(headers, key, def, id); } size_t Request::get_header_value_count(const std::string &key) const { return detail::get_header_value_count(headers, key); } void Request::set_header(const std::string &key, const std::string &val) { detail::set_header(headers, key, val); } bool Request::has_trailer(const std::string &key) const { return trailers.find(key) != trailers.end(); } std::string Request::get_trailer_value(const std::string &key, size_t id) const { return detail::get_multimap_value(trailers, key, id); } size_t Request::get_trailer_value_count(const std::string &key) const { auto r = trailers.equal_range(key); return static_cast(std::distance(r.first, r.second)); } bool Request::has_param(const std::string &key) const { return params.find(key) != params.end(); } std::string Request::get_param_value(const std::string &key, size_t id) const { return detail::get_multimap_value(params, key, id); } std::vector Request::get_param_values(const std::string &key) const { auto rng = params.equal_range(key); std::vector values; values.reserve(static_cast(std::distance(rng.first, rng.second))); for (auto it = rng.first; it != rng.second; ++it) { values.push_back(it->second); } return values; } size_t Request::get_param_value_count(const std::string &key) const { auto r = params.equal_range(key); return static_cast(std::distance(r.first, r.second)); } bool Request::is_multipart_form_data() const { const auto &content_type = get_header_value("Content-Type"); return detail::extract_media_type(content_type) == "multipart/form-data"; } // Multipart FormData implementation std::string MultipartFormData::get_field(const std::string &key, size_t id) const { auto rng = fields.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second.content; } return std::string(); } std::vector MultipartFormData::get_fields(const std::string &key) const { std::vector values; auto rng = fields.equal_range(key); for (auto it = rng.first; it != rng.second; it++) { values.push_back(it->second.content); } return values; } bool MultipartFormData::has_field(const std::string &key) const { return fields.find(key) != fields.end(); } size_t MultipartFormData::get_field_count(const std::string &key) const { auto r = fields.equal_range(key); return static_cast(std::distance(r.first, r.second)); } FormData MultipartFormData::get_file(const std::string &key, size_t id) const { return detail::get_multimap_value(files, key, id); } std::vector MultipartFormData::get_files(const std::string &key) const { std::vector values; auto rng = files.equal_range(key); for (auto it = rng.first; it != rng.second; it++) { values.push_back(it->second); } return values; } bool MultipartFormData::has_file(const std::string &key) const { return files.find(key) != files.end(); } size_t MultipartFormData::get_file_count(const std::string &key) const { auto r = files.equal_range(key); return static_cast(std::distance(r.first, r.second)); } // Response implementation size_t Response::get_header_value_u64(const std::string &key, size_t def, size_t id) const { return detail::get_header_value_u64(headers, key, def, id); } bool Response::has_header(const std::string &key) const { return headers.find(key) != headers.end(); } std::string Response::get_header_value(const std::string &key, const char *def, size_t id) const { return detail::get_header_value(headers, key, def, id); } size_t Response::get_header_value_count(const std::string &key) const { return detail::get_header_value_count(headers, key); } void Response::set_header(const std::string &key, const std::string &val) { detail::set_header(headers, key, val); } bool Response::has_trailer(const std::string &key) const { return trailers.find(key) != trailers.end(); } std::string Response::get_trailer_value(const std::string &key, size_t id) const { return detail::get_multimap_value(trailers, key, id); } size_t Response::get_trailer_value_count(const std::string &key) const { auto r = trailers.equal_range(key); return static_cast(std::distance(r.first, r.second)); } void Response::set_redirect(const std::string &url, int stat) { if (detail::fields::is_field_value(url)) { set_header("Location", url); if (300 <= stat && stat < 400) { this->status = stat; } else { this->status = StatusCode::Found_302; } } } void Response::set_content(const char *s, size_t n, const std::string &content_type) { body.assign(s, n); auto rng = headers.equal_range("Content-Type"); headers.erase(rng.first, rng.second); set_header("Content-Type", content_type); } void Response::set_content(const std::string &s, const std::string &content_type) { set_content(s.data(), s.size(), content_type); } void Response::set_content(std::string &&s, const std::string &content_type) { body = std::move(s); auto rng = headers.equal_range("Content-Type"); headers.erase(rng.first, rng.second); set_header("Content-Type", content_type); } void Response::set_content_provider( size_t in_length, const std::string &content_type, ContentProvider provider, ContentProviderResourceReleaser resource_releaser) { set_header("Content-Type", content_type); content_length_ = in_length; if (in_length > 0) { content_provider_ = std::move(provider); } content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = false; } void Response::set_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser) { set_header("Content-Type", content_type); content_length_ = 0; content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = false; } void Response::set_chunked_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser) { set_header("Content-Type", content_type); content_length_ = 0; content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = std::move(resource_releaser); is_chunked_content_provider_ = true; } void Response::set_file_content(const std::string &path, const std::string &content_type) { file_content_path_ = path; file_content_content_type_ = content_type; } void Response::set_file_content(const std::string &path) { file_content_path_ = path; } // Result implementation size_t Result::get_request_header_value_u64(const std::string &key, size_t def, size_t id) const { return detail::get_header_value_u64(request_headers_, key, def, id); } bool Result::has_request_header(const std::string &key) const { return request_headers_.find(key) != request_headers_.end(); } std::string Result::get_request_header_value(const std::string &key, const char *def, size_t id) const { return detail::get_header_value(request_headers_, key, def, id); } size_t Result::get_request_header_value_count(const std::string &key) const { auto r = request_headers_.equal_range(key); return static_cast(std::distance(r.first, r.second)); } // Stream implementation ssize_t Stream::write(const char *ptr) { return write(ptr, strlen(ptr)); } ssize_t Stream::write(const std::string &s) { return write(s.data(), s.size()); } // BodyReader implementation ssize_t detail::BodyReader::read(char *buf, size_t len) { if (!stream) { last_error = Error::Connection; return -1; } if (eof) { return 0; } if (!chunked) { // Content-Length based reading if (has_content_length && bytes_read >= content_length) { eof = true; return 0; } auto to_read = len; if (has_content_length) { auto remaining = content_length - bytes_read; to_read = (std::min)(len, remaining); } auto n = stream->read(buf, to_read); if (n < 0) { last_error = stream->get_error(); if (last_error == Error::Success) { last_error = Error::Read; } eof = true; return n; } if (n == 0) { // Unexpected EOF before content_length last_error = stream->get_error(); if (last_error == Error::Success) { last_error = Error::Read; } eof = true; return 0; } bytes_read += static_cast(n); if (has_content_length && bytes_read >= content_length) { eof = true; } if (payload_max_length > 0 && bytes_read > payload_max_length) { last_error = Error::ExceedMaxPayloadSize; eof = true; return -1; } return n; } // Chunked transfer encoding: delegate to shared decoder instance. if (!chunked_decoder) { chunked_decoder.reset(new ChunkedDecoder(*stream)); } size_t chunk_offset = 0; size_t chunk_total = 0; auto n = chunked_decoder->read_payload(buf, len, chunk_offset, chunk_total); if (n < 0) { last_error = stream->get_error(); if (last_error == Error::Success) { last_error = Error::Read; } eof = true; return n; } if (n == 0) { // Final chunk observed. Leave trailer parsing to the caller (StreamHandle). eof = true; return 0; } bytes_read += static_cast(n); if (payload_max_length > 0 && bytes_read > payload_max_length) { last_error = Error::ExceedMaxPayloadSize; eof = true; return -1; } return n; } // ThreadPool implementation ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr) : base_thread_count_(n), max_queued_requests_(mqr), idle_thread_count_(0), shutdown_(false) { #ifndef CPPHTTPLIB_NO_EXCEPTIONS if (max_n != 0 && max_n < n) { std::string msg = "max_threads must be >= base_threads"; throw std::invalid_argument(msg); } #endif max_thread_count_ = max_n == 0 ? n : max_n; threads_.reserve(base_thread_count_); #ifndef CPPHTTPLIB_NO_EXCEPTIONS try { #endif for (size_t i = 0; i < base_thread_count_; i++) { threads_.emplace_back(std::thread([this]() { worker(false); })); } #ifndef CPPHTTPLIB_NO_EXCEPTIONS } catch (...) { // If thread creation fails partway (e.g., pthread_create returns EAGAIN), // signal the workers we already spawned to exit and join them so the // vector destructor does not see joinable threads (which would call // std::terminate). Then rethrow so the caller learns of the failure. { std::unique_lock lock(mutex_); shutdown_ = true; } cond_.notify_all(); for (auto &t : threads_) { if (t.joinable()) { t.join(); } } throw; } #endif } bool ThreadPool::enqueue(std::function fn) { { std::unique_lock lock(mutex_); if (shutdown_) { return false; } if (max_queued_requests_ > 0 && jobs_.size() >= max_queued_requests_) { return false; } jobs_.push_back(std::move(fn)); // Spawn a dynamic thread if no idle threads and under max if (idle_thread_count_ == 0 && threads_.size() + dynamic_threads_.size() < max_thread_count_) { cleanup_finished_threads(); dynamic_threads_.emplace_back(std::thread([this]() { worker(true); })); } } cond_.notify_one(); return true; } void ThreadPool::shutdown() { { std::unique_lock lock(mutex_); shutdown_ = true; } cond_.notify_all(); for (auto &t : threads_) { if (t.joinable()) { t.join(); } } // Move dynamic_threads_ to a local list under the lock to avoid racing // with worker threads that call move_to_finished() concurrently. std::list remaining_dynamic; { std::unique_lock lock(mutex_); remaining_dynamic = std::move(dynamic_threads_); } for (auto &t : remaining_dynamic) { if (t.joinable()) { t.join(); } } std::unique_lock lock(mutex_); cleanup_finished_threads(); } void ThreadPool::move_to_finished(std::thread::id id) { // Must be called with mutex_ held for (auto it = dynamic_threads_.begin(); it != dynamic_threads_.end(); ++it) { if (it->get_id() == id) { finished_threads_.push_back(std::move(*it)); dynamic_threads_.erase(it); return; } } } void ThreadPool::cleanup_finished_threads() { // Must be called with mutex_ held for (auto &t : finished_threads_) { if (t.joinable()) { t.join(); } } finished_threads_.clear(); } void ThreadPool::worker(bool is_dynamic) { for (;;) { std::function fn; { std::unique_lock lock(mutex_); idle_thread_count_++; if (is_dynamic) { auto has_work = cond_.wait_for( lock, std::chrono::seconds(CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT), [&] { return !jobs_.empty() || shutdown_; }); if (!has_work) { // Timed out with no work - exit this dynamic thread idle_thread_count_--; move_to_finished(std::this_thread::get_id()); break; } } else { cond_.wait(lock, [&] { return !jobs_.empty() || shutdown_; }); } idle_thread_count_--; if (shutdown_ && jobs_.empty()) { break; } fn = std::move(jobs_.front()); jobs_.pop_front(); } assert(true == static_cast(fn)); fn(); } #if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(OPENSSL_IS_BORINGSSL) && \ !defined(LIBRESSL_VERSION_NUMBER) OPENSSL_thread_stop(); #endif } /* * Group 1 (continued): detail namespace - Stream implementations */ namespace detail { void calc_actual_timeout(time_t max_timeout_msec, time_t duration_msec, time_t timeout_sec, time_t timeout_usec, time_t &actual_timeout_sec, time_t &actual_timeout_usec) { auto timeout_msec = (timeout_sec * 1000) + (timeout_usec / 1000); auto actual_timeout_msec = (std::min)(max_timeout_msec - duration_msec, timeout_msec); if (actual_timeout_msec < 0) { actual_timeout_msec = 0; } actual_timeout_sec = actual_timeout_msec / 1000; actual_timeout_usec = (actual_timeout_msec % 1000) * 1000; } // Socket stream implementation SocketStream::SocketStream( socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, std::chrono::time_point start_time) : sock_(sock), read_timeout_sec_(read_timeout_sec), read_timeout_usec_(read_timeout_usec), write_timeout_sec_(write_timeout_sec), write_timeout_usec_(write_timeout_usec), max_timeout_msec_(max_timeout_msec), start_time_(start_time), read_buff_(read_buff_size_, 0) {} SocketStream::~SocketStream() = default; bool SocketStream::is_readable() const { return read_buff_off_ < read_buff_content_size_; } bool SocketStream::wait_readable() const { if (max_timeout_msec_ <= 0) { return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; } time_t read_timeout_sec; time_t read_timeout_usec; calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, read_timeout_usec_, read_timeout_sec, read_timeout_usec); return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; } bool SocketStream::wait_writable() const { return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0; } bool SocketStream::is_peer_alive() const { return detail::is_socket_alive(sock_); } ssize_t SocketStream::read(char *ptr, size_t size) { #ifdef _WIN32 size = (std::min)(size, static_cast((std::numeric_limits::max)())); #else size = (std::min)(size, static_cast((std::numeric_limits::max)())); #endif if (read_buff_off_ < read_buff_content_size_) { auto remaining_size = read_buff_content_size_ - read_buff_off_; if (size <= remaining_size) { memcpy(ptr, read_buff_.data() + read_buff_off_, size); read_buff_off_ += size; return static_cast(size); } else { memcpy(ptr, read_buff_.data() + read_buff_off_, remaining_size); read_buff_off_ += remaining_size; return static_cast(remaining_size); } } if (!wait_readable()) { error_ = Error::Timeout; return -1; } read_buff_off_ = 0; read_buff_content_size_ = 0; if (size < read_buff_size_) { auto n = read_socket(sock_, read_buff_.data(), read_buff_size_, CPPHTTPLIB_RECV_FLAGS); if (n <= 0) { if (n == 0) { error_ = Error::ConnectionClosed; } else { error_ = Error::Read; } return n; } else if (n <= static_cast(size)) { memcpy(ptr, read_buff_.data(), static_cast(n)); return n; } else { memcpy(ptr, read_buff_.data(), size); read_buff_off_ = size; read_buff_content_size_ = static_cast(n); return static_cast(size); } } else { auto n = read_socket(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS); if (n <= 0) { if (n == 0) { error_ = Error::ConnectionClosed; } else { error_ = Error::Read; } } return n; } } ssize_t SocketStream::write(const char *ptr, size_t size) { if (!wait_writable()) { return -1; } #if defined(_WIN32) && !defined(_WIN64) size = (std::min)(size, static_cast((std::numeric_limits::max)())); #endif return send_socket(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS); } void SocketStream::get_remote_ip_and_port(std::string &ip, int &port) const { return detail::get_remote_ip_and_port(sock_, ip, port); } void SocketStream::get_local_ip_and_port(std::string &ip, int &port) const { return detail::get_local_ip_and_port(sock_, ip, port); } socket_t SocketStream::socket() const { return sock_; } time_t SocketStream::duration() const { return std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time_) .count(); } void SocketStream::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; } // Buffer stream implementation bool BufferStream::is_readable() const { return true; } bool BufferStream::wait_readable() const { return true; } bool BufferStream::wait_writable() const { return true; } ssize_t BufferStream::read(char *ptr, size_t size) { #if defined(_MSC_VER) && _MSC_VER < 1910 auto len_read = buffer._Copy_s(ptr, size, size, position); #else auto len_read = buffer.copy(ptr, size, position); #endif position += static_cast(len_read); return static_cast(len_read); } ssize_t BufferStream::write(const char *ptr, size_t size) { buffer.append(ptr, size); return static_cast(size); } void BufferStream::get_remote_ip_and_port(std::string & /*ip*/, int & /*port*/) const {} void BufferStream::get_local_ip_and_port(std::string & /*ip*/, int & /*port*/) const {} socket_t BufferStream::socket() const { return 0; } time_t BufferStream::duration() const { return 0; } const std::string &BufferStream::get_buffer() const { return buffer; } PathParamsMatcher::PathParamsMatcher(const std::string &pattern) : MatcherBase(pattern) { constexpr const char marker[] = "/:"; // One past the last ending position of a path param substring std::size_t last_param_end = 0; #ifndef CPPHTTPLIB_NO_EXCEPTIONS // Needed to ensure that parameter names are unique during matcher // construction // If exceptions are disabled, only last duplicate path // parameter will be set std::unordered_set param_name_set; #endif while (true) { const auto marker_pos = pattern.find( marker, last_param_end == 0 ? last_param_end : last_param_end - 1); if (marker_pos == std::string::npos) { break; } static_fragments_.push_back( pattern.substr(last_param_end, marker_pos - last_param_end + 1)); const auto param_name_start = marker_pos + str_len(marker); auto sep_pos = pattern.find(separator, param_name_start); if (sep_pos == std::string::npos) { sep_pos = pattern.length(); } auto param_name = pattern.substr(param_name_start, sep_pos - param_name_start); #ifndef CPPHTTPLIB_NO_EXCEPTIONS if (param_name_set.find(param_name) != param_name_set.cend()) { std::string msg = "Encountered path parameter '" + param_name + "' multiple times in route pattern '" + pattern + "'."; throw std::invalid_argument(msg); } #endif param_names_.push_back(std::move(param_name)); last_param_end = sep_pos + 1; } if (last_param_end < pattern.length()) { static_fragments_.push_back(pattern.substr(last_param_end)); } } bool PathParamsMatcher::match(Request &request) const { request.matches = std::smatch(); request.path_params.clear(); request.path_params.reserve(param_names_.size()); // One past the position at which the path matched the pattern last time std::size_t starting_pos = 0; for (size_t i = 0; i < static_fragments_.size(); ++i) { const auto &fragment = static_fragments_[i]; if (starting_pos + fragment.length() > request.path.length()) { return false; } // Avoid unnecessary allocation by using strncmp instead of substr + // comparison if (std::strncmp(request.path.c_str() + starting_pos, fragment.c_str(), fragment.length()) != 0) { return false; } starting_pos += fragment.length(); // Should only happen when we have a static fragment after a param // Example: '/users/:id/subscriptions' // The 'subscriptions' fragment here does not have a corresponding param if (i >= param_names_.size()) { continue; } auto sep_pos = request.path.find(separator, starting_pos); if (sep_pos == std::string::npos) { sep_pos = request.path.length(); } const auto ¶m_name = param_names_[i]; request.path_params.emplace( param_name, request.path.substr(starting_pos, sep_pos - starting_pos)); // Mark everything up to '/' as matched starting_pos = sep_pos + 1; } // Returns false if the path is longer than the pattern return starting_pos >= request.path.length(); } bool RegexMatcher::match(Request &request) const { request.path_params.clear(); return std::regex_match(request.path, request.matches, regex_); } // Enclose IPv6 address in brackets if needed std::string prepare_host_string(const std::string &host) { // Enclose IPv6 address in brackets (but not if already enclosed) if (host.find(':') == std::string::npos || (!host.empty() && host[0] == '[')) { // IPv4, hostname, or already bracketed IPv6 return host; } else { // IPv6 address without brackets return "[" + host + "]"; } } std::string make_host_and_port_string(const std::string &host, int port, bool is_ssl) { auto result = prepare_host_string(host); // Append port if not default if ((!is_ssl && port == 80) || (is_ssl && port == 443)) { ; // do nothing } else { result += ":" + std::to_string(port); } return result; } // Create "host:port" string always including port number (for CONNECT method) std::string make_host_and_port_string_always_port(const std::string &host, int port) { return prepare_host_string(host) + ":" + std::to_string(port); } 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 host_matches_no_proxy(const NormalizedTarget &target, const std::vector &entries); bool ip_in_cidr(const IPBytes &ip, const IPBytes &net, int prefix_bits) { if (prefix_bits < 0 || prefix_bits > 128) { return false; } if (prefix_bits == 0) { return true; } int full_bytes = prefix_bits / 8; int rem_bits = prefix_bits % 8; if (full_bytes > 0 && std::memcmp(ip.data(), net.data(), static_cast(full_bytes)) != 0) { return false; } if (rem_bits == 0) { return true; } auto i = static_cast(full_bytes); auto mask = static_cast(0xFFu << (8 - rem_bits)); return (ip[i] & mask) == (net[i] & mask); } bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out) { if (token.empty()) { return false; } if (token == "*") { out.kind = NoProxyKind::Wildcard; return true; } auto slash = token.find('/'); std::string addr_part = (slash == std::string::npos) ? token : token.substr(0, slash); std::string prefix_part = (slash == std::string::npos) ? std::string() : token.substr(slash + 1); // A bare slash or trailing-slash CIDR like "10.0.0.0/" is malformed; // don't silently treat it as a /32 (or /128). if (slash != std::string::npos && prefix_part.empty()) { return false; } // Accept the bracketed IPv6 form ("[::1]", "[fe80::]/10") as well as the // bare form. Brackets have no meaning for IPv4, so skip the IPv4 attempt // when brackets are present. bool bracketed = addr_part.size() >= 2 && addr_part.front() == '[' && addr_part.back() == ']'; if (bracketed) { addr_part = addr_part.substr(1, addr_part.size() - 2); } if (!bracketed) { struct in_addr v4; if (inet_pton(AF_INET, addr_part.c_str(), &v4) == 1) { int prefix = 32; if (!prefix_part.empty()) { auto r = from_chars(prefix_part.data(), prefix_part.data() + prefix_part.size(), prefix); if (r.ec != std::errc{} || r.ptr != prefix_part.data() + prefix_part.size()) { return false; } if (prefix < 0 || prefix > 32) { return false; } } out.kind = NoProxyKind::IPv4Cidr; std::memcpy(out.net.data(), &v4, sizeof(v4)); out.prefix_bits = prefix; return true; } } struct in6_addr v6; if (inet_pton(AF_INET6, addr_part.c_str(), &v6) == 1) { int prefix = 128; if (!prefix_part.empty()) { auto r = from_chars(prefix_part.data(), prefix_part.data() + prefix_part.size(), prefix); if (r.ec != std::errc{} || r.ptr != prefix_part.data() + prefix_part.size()) { return false; } if (prefix < 0 || prefix > 128) { return false; } } out.kind = NoProxyKind::IPv6Cidr; std::memcpy(out.net.data(), &v6, sizeof(v6)); out.prefix_bits = prefix; return true; } // Bracketed entries can only be IPv6. If the IPv6 parse above failed, // the entry is malformed โ€” don't fall through to the hostname branch. if (bracketed) { return false; } // A '/' on a non-IP token means a CIDR prefix without an address. Reject. if (slash != std::string::npos) { return false; } // Port-specific entries (host:port) are not supported. if (token.find(':') != std::string::npos) { return false; } std::string hostname = case_ignore::to_lower(token); while (!hostname.empty() && hostname.front() == '.') { hostname.erase(hostname.begin()); } while (!hostname.empty() && hostname.back() == '.') { hostname.pop_back(); } if (hostname.empty()) { return false; } out.kind = NoProxyKind::HostnameSuffix; out.hostname_pattern = std::move(hostname); return true; } NormalizedTarget normalize_target(const std::string &host) { NormalizedTarget t; std::string h = host; if (h.size() >= 2 && h.front() == '[' && h.back() == ']') { h = h.substr(1, h.size() - 2); } // Strip a single trailing dot so "example.com." canonicalizes to // "example.com". if (!h.empty() && h.back() == '.') { h.pop_back(); } t.hostname = case_ignore::to_lower(h); if (!t.hostname.empty()) { struct in_addr v4; struct in6_addr v6; if (inet_pton(AF_INET, t.hostname.c_str(), &v4) == 1) { t.is_ipv4 = true; std::memcpy(t.ip.data(), &v4, sizeof(v4)); } else if (inet_pton(AF_INET6, t.hostname.c_str(), &v6) == 1) { t.is_ipv6 = true; std::memcpy(t.ip.data(), &v6, sizeof(v6)); } } return t; } bool host_matches_no_proxy(const NormalizedTarget &target, const std::vector &entries) { if (target.hostname.empty()) { return false; } for (const auto &e : entries) { switch (e.kind) { case NoProxyKind::Wildcard: return true; case NoProxyKind::IPv4Cidr: if (target.is_ipv4 && ip_in_cidr(target.ip, e.net, e.prefix_bits)) { return true; } break; case NoProxyKind::IPv6Cidr: if (target.is_ipv6 && ip_in_cidr(target.ip, e.net, e.prefix_bits)) { return true; } break; case NoProxyKind::HostnameSuffix: if (target.is_ipv4 || target.is_ipv6) { break; } if (target.hostname == e.hostname_pattern) { return true; } // Dot-boundary suffix match: prevents "evilexample.com" from matching // an entry of "example.com". if (target.hostname.size() > e.hostname_pattern.size() + 1) { auto offset = target.hostname.size() - e.hostname_pattern.size(); if (target.hostname[offset - 1] == '.' && target.hostname.compare(offset, e.hostname_pattern.size(), e.hostname_pattern) == 0) { return true; } } break; } } return false; } template bool check_and_write_headers(Stream &strm, Headers &headers, T header_writer, Error &error) { for (const auto &h : headers) { if (!detail::fields::is_field_name(h.first) || !detail::fields::is_field_value(h.second)) { error = Error::InvalidHeaders; return false; } } if (header_writer(strm, headers) <= 0) { error = Error::Write; return false; } return true; } } // namespace detail /* * Group 2 (continued): detail namespace - SSLSocketStream implementation */ #ifdef CPPHTTPLIB_SSL_ENABLED namespace detail { // SSL socket stream implementation SSLSocketStream::SSLSocketStream( socket_t sock, tls::session_t session, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec, std::chrono::time_point start_time) : sock_(sock), session_(session), read_timeout_sec_(read_timeout_sec), read_timeout_usec_(read_timeout_usec), write_timeout_sec_(write_timeout_sec), write_timeout_usec_(write_timeout_usec), max_timeout_msec_(max_timeout_msec), start_time_(start_time) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // Clear AUTO_RETRY for proper non-blocking I/O timeout handling // Note: create_session() also clears this, but SSLClient currently // uses ssl_new() which does not. Until full TLS API migration is complete, // we need to ensure AUTO_RETRY is cleared here regardless of how the // SSL session was created. SSL_clear_mode(static_cast(session), SSL_MODE_AUTO_RETRY); #endif } SSLSocketStream::~SSLSocketStream() = default; bool SSLSocketStream::is_readable() const { return tls::pending(session_) > 0; } bool SSLSocketStream::wait_readable() const { if (max_timeout_msec_ <= 0) { return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; } time_t read_timeout_sec; time_t read_timeout_usec; calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, read_timeout_usec_, read_timeout_sec, read_timeout_usec); return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; } bool SSLSocketStream::wait_writable() const { return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && !tls::is_peer_closed(session_, sock_); } bool SSLSocketStream::is_peer_alive() const { return !tls::is_peer_closed(session_, sock_); } ssize_t SSLSocketStream::read(char *ptr, size_t size) { if (tls::pending(session_) > 0) { tls::TlsError err; auto ret = tls::read(session_, ptr, size, err); if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { error_ = Error::ConnectionClosed; } return ret; } else if (wait_readable()) { tls::TlsError err; auto ret = tls::read(session_, ptr, size, err); if (ret < 0) { auto n = 1000; #ifdef _WIN32 while (--n >= 0 && (err.code == tls::ErrorCode::WantRead || (err.code == tls::ErrorCode::SyscallError && WSAGetLastError() == WSAETIMEDOUT))) { #else while (--n >= 0 && err.code == tls::ErrorCode::WantRead) { #endif if (tls::pending(session_) > 0) { return tls::read(session_, ptr, size, err); } else if (wait_readable()) { std::this_thread::sleep_for(std::chrono::microseconds{10}); ret = tls::read(session_, ptr, size, err); if (ret >= 0) { return ret; } } else { break; } } assert(ret < 0); } else if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { error_ = Error::ConnectionClosed; } return ret; } else { error_ = Error::Timeout; return -1; } } ssize_t SSLSocketStream::write(const char *ptr, size_t size) { if (wait_writable()) { auto handle_size = std::min(size, (std::numeric_limits::max)()); tls::TlsError err; auto ret = tls::write(session_, ptr, handle_size, err); if (ret < 0) { auto n = 1000; #ifdef _WIN32 while (--n >= 0 && (err.code == tls::ErrorCode::WantWrite || (err.code == tls::ErrorCode::SyscallError && WSAGetLastError() == WSAETIMEDOUT))) { #else while (--n >= 0 && err.code == tls::ErrorCode::WantWrite) { #endif if (wait_writable()) { std::this_thread::sleep_for(std::chrono::microseconds{10}); ret = tls::write(session_, ptr, handle_size, err); if (ret >= 0) { return ret; } } else { break; } } assert(ret < 0); } return ret; } return -1; } void SSLSocketStream::get_remote_ip_and_port(std::string &ip, int &port) const { detail::get_remote_ip_and_port(sock_, ip, port); } void SSLSocketStream::get_local_ip_and_port(std::string &ip, int &port) const { detail::get_local_ip_and_port(sock_, ip, port); } socket_t SSLSocketStream::socket() const { return sock_; } time_t SSLSocketStream::duration() const { return std::chrono::duration_cast( std::chrono::steady_clock::now() - start_time_) .count(); } void SSLSocketStream::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; } } // namespace detail #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 4: Server implementation */ // HTTP server implementation Server::Server() : new_task_queue([] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT, CPPHTTPLIB_THREAD_POOL_MAX_COUNT); }) { #ifndef _WIN32 signal(SIGPIPE, SIG_IGN); #endif } Server::~Server() = default; std::unique_ptr Server::make_matcher(const std::string &pattern) { if (pattern.find("/:") != std::string::npos) { return detail::make_unique(pattern); } else { return detail::make_unique(pattern); } } Server &Server::Get(const std::string &pattern, Handler handler) { return add_handler(get_handlers_, pattern, std::move(handler)); } Server &Server::Post(const std::string &pattern, Handler handler) { return add_handler(post_handlers_, pattern, std::move(handler)); } Server &Server::Post(const std::string &pattern, HandlerWithContentReader handler) { return add_handler(post_handlers_for_content_reader_, pattern, std::move(handler)); } Server &Server::Put(const std::string &pattern, Handler handler) { return add_handler(put_handlers_, pattern, std::move(handler)); } Server &Server::Put(const std::string &pattern, HandlerWithContentReader handler) { return add_handler(put_handlers_for_content_reader_, pattern, std::move(handler)); } Server &Server::Patch(const std::string &pattern, Handler handler) { return add_handler(patch_handlers_, pattern, std::move(handler)); } Server &Server::Patch(const std::string &pattern, HandlerWithContentReader handler) { return add_handler(patch_handlers_for_content_reader_, pattern, std::move(handler)); } Server &Server::Delete(const std::string &pattern, Handler handler) { return add_handler(delete_handlers_, pattern, std::move(handler)); } Server &Server::Delete(const std::string &pattern, HandlerWithContentReader handler) { return add_handler(delete_handlers_for_content_reader_, pattern, std::move(handler)); } Server &Server::Options(const std::string &pattern, Handler handler) { return add_handler(options_handlers_, pattern, std::move(handler)); } Server &Server::WebSocket(const std::string &pattern, WebSocketHandler handler) { websocket_handlers_.push_back( {make_matcher(pattern), std::move(handler), nullptr}); return *this; } Server &Server::WebSocket(const std::string &pattern, WebSocketHandler handler, SubProtocolSelector sub_protocol_selector) { websocket_handlers_.push_back({make_matcher(pattern), std::move(handler), std::move(sub_protocol_selector)}); return *this; } bool Server::set_base_dir(const std::string &dir, const std::string &mount_point) { return set_mount_point(mount_point, dir); } bool Server::set_mount_point(const std::string &mount_point, const std::string &dir, Headers headers) { detail::FileStat stat(dir); if (stat.is_dir()) { std::string mnt = !mount_point.empty() ? mount_point : "/"; if (!mnt.empty() && mnt[0] == '/') { std::string resolved_base; if (detail::canonicalize_path(dir.c_str(), resolved_base)) { #if defined(_WIN32) if (resolved_base.back() != '\\' && resolved_base.back() != '/') { resolved_base += '\\'; } #else if (resolved_base.back() != '/') { resolved_base += '/'; } #endif } base_dirs_.push_back( {std::move(mnt), dir, std::move(resolved_base), std::move(headers)}); return true; } } return false; } bool Server::remove_mount_point(const std::string &mount_point) { for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) { if (it->mount_point == mount_point) { base_dirs_.erase(it); return true; } } return false; } Server & Server::set_file_extension_and_mimetype_mapping(const std::string &ext, const std::string &mime) { file_extension_and_mimetype_map_[ext] = mime; return *this; } Server &Server::set_default_file_mimetype(const std::string &mime) { default_file_mimetype_ = mime; return *this; } Server &Server::set_file_request_handler(Handler handler) { file_request_handler_ = std::move(handler); return *this; } Server &Server::set_error_handler_core(HandlerWithResponse handler, std::true_type) { error_handler_ = std::move(handler); return *this; } Server &Server::set_error_handler_core(Handler handler, std::false_type) { error_handler_ = [handler](const Request &req, Response &res) { handler(req, res); return HandlerResponse::Handled; }; return *this; } Server &Server::set_exception_handler(ExceptionHandler handler) { exception_handler_ = std::move(handler); return *this; } Server &Server::set_pre_routing_handler(HandlerWithResponse handler) { pre_routing_handler_ = std::move(handler); return *this; } Server &Server::set_post_routing_handler(Handler handler) { post_routing_handler_ = std::move(handler); return *this; } Server &Server::set_pre_request_handler(HandlerWithResponse handler) { pre_request_handler_ = std::move(handler); return *this; } Server &Server::set_logger(Logger logger) { logger_ = std::move(logger); return *this; } Server &Server::set_error_logger(ErrorLogger error_logger) { error_logger_ = std::move(error_logger); return *this; } Server &Server::set_pre_compression_logger(Logger logger) { pre_compression_logger_ = std::move(logger); return *this; } Server & Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { expect_100_continue_handler_ = std::move(handler); return *this; } Server &Server::set_start_handler(StartHandler handler) { start_handler_ = std::move(handler); return *this; } Server &Server::set_address_family(int family) { address_family_ = family; return *this; } Server &Server::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; return *this; } Server &Server::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; return *this; } Server &Server::set_socket_options(SocketOptions socket_options) { socket_options_ = std::move(socket_options); return *this; } Server &Server::set_default_headers(Headers headers) { default_headers_ = std::move(headers); return *this; } Server &Server::set_header_writer( std::function const &writer) { header_writer_ = writer; return *this; } Server & Server::set_trusted_proxies(const std::vector &proxies) { trusted_proxies_ = proxies; return *this; } Server &Server::set_keep_alive_max_count(size_t count) { keep_alive_max_count_ = count; return *this; } Server &Server::set_keep_alive_timeout(time_t sec) { keep_alive_timeout_sec_ = sec; return *this; } template Server &Server::set_keep_alive_timeout( const std::chrono::duration &duration) { detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t /*usec*/) { set_keep_alive_timeout(sec); }); return *this; } Server &Server::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; return *this; } Server &Server::set_write_timeout(time_t sec, time_t usec) { write_timeout_sec_ = sec; write_timeout_usec_ = usec; return *this; } Server &Server::set_idle_interval(time_t sec, time_t usec) { idle_interval_sec_ = sec; idle_interval_usec_ = usec; return *this; } Server &Server::set_payload_max_length(size_t length) { payload_max_length_ = length; return *this; } Server &Server::set_websocket_max_missed_pongs(int count) { websocket_max_missed_pongs_ = count; return *this; } Server &Server::set_websocket_ping_interval(time_t sec) { websocket_ping_interval_sec_ = sec; return *this; } template Server &Server::set_websocket_ping_interval( const std::chrono::duration &duration) { detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t /*usec*/) { set_websocket_ping_interval(sec); }); return *this; } bool Server::bind_to_port(const std::string &host, int port, int socket_flags) { auto ret = bind_internal(host, port, socket_flags); if (ret == -1) { is_decommissioned = true; } return ret >= 0; } int Server::bind_to_any_port(const std::string &host, int socket_flags) { auto ret = bind_internal(host, 0, socket_flags); if (ret == -1) { is_decommissioned = true; } return ret; } bool Server::listen_after_bind() { return listen_internal(); } bool Server::listen(const std::string &host, int port, int socket_flags) { return bind_to_port(host, port, socket_flags) && listen_internal(); } bool Server::is_running() const { return is_running_; } void Server::wait_until_ready() const { while (!is_running_ && !is_decommissioned) { std::this_thread::sleep_for(std::chrono::milliseconds{1}); } } void Server::stop() noexcept { if (is_running_) { assert(svr_sock_ != INVALID_SOCKET); std::atomic sock(svr_sock_.exchange(INVALID_SOCKET)); detail::shutdown_socket(sock); detail::close_socket(sock); } is_decommissioned = false; } void Server::decommission() { is_decommissioned = true; } bool Server::parse_request_line(const char *s, Request &req) const { auto len = strlen(s); if (len < 2 || s[len - 2] != '\r' || s[len - 1] != '\n') { return false; } len -= 2; { size_t count = 0; detail::split(s, s + len, ' ', [&](const char *b, const char *e) { switch (count) { case 0: req.method = std::string(b, e); break; case 1: req.target = std::string(b, e); break; case 2: req.version = std::string(b, e); break; default: break; } count++; }); if (count != 3) { return false; } } thread_local const std::set methods{ "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; if (methods.find(req.method) == methods.end()) { output_error_log(Error::InvalidHTTPMethod, &req); return false; } if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { output_error_log(Error::InvalidHTTPVersion, &req); return false; } { // Skip URL fragment for (size_t i = 0; i < req.target.size(); i++) { if (req.target[i] == '#') { req.target.erase(i); break; } } detail::divide(req.target, '?', [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, std::size_t rhs_size) { req.path = decode_path_component(std::string(lhs_data, lhs_size)); detail::parse_query_text(rhs_data, rhs_size, req.params); }); } return true; } bool Server::write_response(Stream &strm, bool close_connection, Request &req, Response &res) { // NOTE: `req.ranges` should be empty, otherwise it will be applied // incorrectly to the error content. req.ranges.clear(); return write_response_core(strm, close_connection, req, res, false); } bool Server::write_response_with_content(Stream &strm, bool close_connection, const Request &req, Response &res) { return write_response_core(strm, close_connection, req, res, true); } bool Server::write_response_core(Stream &strm, bool close_connection, const Request &req, Response &res, bool need_apply_ranges) { assert(res.status != -1); if (400 <= res.status && error_handler_ && error_handler_(req, res) == HandlerResponse::Handled) { need_apply_ranges = true; } std::string content_type; std::string boundary; if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); } // Prepare additional headers if (close_connection || req.get_header_value("Connection") == "close" || 400 <= res.status) { // Don't leave connections open after errors res.set_header("Connection", "close"); } else { std::string s = "timeout="; s += std::to_string(keep_alive_timeout_sec_); s += ", max="; s += std::to_string(keep_alive_max_count_); res.set_header("Keep-Alive", s); } if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) && !res.has_header("Content-Type")) { res.set_header("Content-Type", "text/plain"); } if (res.body.empty() && !res.content_length_ && !res.content_provider_ && !res.has_header("Content-Length")) { res.set_header("Content-Length", "0"); } if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) { res.set_header("Accept-Ranges", "bytes"); } if (post_routing_handler_) { post_routing_handler_(req, res); } // Response line and headers detail::BufferStream bstrm; if (!detail::write_response_line(bstrm, res.status)) { return false; } if (header_writer_(bstrm, res.headers) <= 0) { return false; } // Combine small body with headers to reduce write syscalls if (req.method != "HEAD" && !res.body.empty() && !res.content_provider_) { bstrm.write(res.body.data(), res.body.size()); } // Log before writing to avoid race condition with client-side code that // accesses logger-captured data immediately after receiving the response. output_log(req, res); // Flush buffer auto &data = bstrm.get_buffer(); if (!detail::write_data(strm, data.data(), data.size())) { return false; } // Streaming body auto ret = true; if (req.method != "HEAD" && res.content_provider_) { if (write_content_with_provider(strm, req, res, boundary, content_type)) { res.content_provider_success_ = true; } else { ret = false; } } return ret; } bool Server::write_content_with_provider(Stream &strm, const Request &req, Response &res, const std::string &boundary, const std::string &content_type) { auto is_shutting_down = [this]() { return this->svr_sock_ == INVALID_SOCKET; }; if (res.content_length_ > 0) { if (req.ranges.empty()) { return detail::write_content(strm, res.content_provider_, 0, res.content_length_, is_shutting_down); } else if (req.ranges.size() == 1) { auto offset_and_length = detail::get_range_offset_and_length( req.ranges[0], res.content_length_); return detail::write_content(strm, res.content_provider_, offset_and_length.first, offset_and_length.second, is_shutting_down); } else { return detail::write_multipart_ranges_data( strm, req, res, boundary, content_type, res.content_length_, is_shutting_down); } } else { if (res.is_chunked_content_provider_) { auto type = detail::encoding_type(req, res); auto compressor = detail::make_compressor(type); if (!compressor) { compressor = detail::make_unique(); } return detail::write_content_chunked(strm, res.content_provider_, is_shutting_down, *compressor); } else { return detail::write_content_without_length(strm, res.content_provider_, is_shutting_down); } } } bool Server::read_content(Stream &strm, Request &req, Response &res) { FormFields::iterator cur_field; FormFiles::iterator cur_file; auto is_text_field = false; size_t count = 0; if (read_content_core( strm, req, res, // Regular [&](const char *buf, size_t n) { // Prevent arithmetic overflow when checking sizes. // Avoid computing (req.body.size() + n) directly because // adding two unsigned `size_t` values can wrap around and // produce a small result instead of indicating overflow. // Instead, check using subtraction: ensure `n` does not // exceed the remaining capacity `max_size() - size()`. if (req.body.size() >= req.body.max_size() || n > req.body.max_size() - req.body.size()) { return false; } // Limit decompressed body size to payload_max_length_ to protect // against "zip bomb" attacks where a small compressed payload // decompresses to a massive size. if (payload_max_length_ > 0 && (req.body.size() >= payload_max_length_ || n > payload_max_length_ - req.body.size())) { return false; } req.body.append(buf, n); return true; }, // Multipart FormData [&](const FormData &file) { if (count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) { output_error_log(Error::TooManyFormDataFiles, &req); return false; } if (file.filename.empty()) { cur_field = req.form.fields.emplace( file.name, FormField{file.name, file.content, file.headers}); is_text_field = true; } else { cur_file = req.form.files.emplace(file.name, file); is_text_field = false; } return true; }, [&](const char *buf, size_t n) { if (is_text_field) { auto &content = cur_field->second.content; if (content.size() + n > content.max_size()) { return false; } content.append(buf, n); } else { auto &content = cur_file->second.content; if (content.size() + n > content.max_size()) { return false; } content.append(buf, n); } return true; })) { const auto &content_type = req.get_header_value("Content-Type"); if (detail::extract_media_type(content_type) == "application/x-www-form-urlencoded") { if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) { res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414? output_error_log(Error::ExceedMaxPayloadSize, &req); return false; } detail::parse_query_text(req.body, req.params); } return true; } return false; } bool Server::read_content_with_content_receiver( Stream &strm, Request &req, Response &res, ContentReceiver receiver, FormDataHeader multipart_header, ContentReceiver multipart_receiver) { return read_content_core(strm, req, res, std::move(receiver), std::move(multipart_header), std::move(multipart_receiver)); } bool Server::read_content_core( Stream &strm, Request &req, Response &res, ContentReceiver receiver, FormDataHeader multipart_header, ContentReceiver multipart_receiver) const { detail::FormDataParser multipart_form_data_parser; ContentReceiverWithProgress out; if (req.is_multipart_form_data()) { const auto &content_type = req.get_header_value("Content-Type"); std::string boundary; if (!detail::parse_multipart_boundary(content_type, boundary)) { res.status = StatusCode::BadRequest_400; output_error_log(Error::MultipartParsing, &req); return false; } multipart_form_data_parser.set_boundary(std::move(boundary)); out = [&](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) { return multipart_form_data_parser.parse(buf, n, multipart_header, multipart_receiver); }; } else { out = [receiver](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) { return receiver(buf, n); }; } // RFC 9112 ยง6: no Transfer-Encoding and no Content-Length means no body. // For non-SSL builds we still scan non-persistent connections for stray // body bytes so the payload limit is enforced (413). On keep-alive, // pending bytes may be the next request (issue #2450), so skip. #if !defined(CPPHTTPLIB_SSL_ENABLED) if (!req.has_header("Content-Length") && !detail::is_chunked_transfer_encoding(req.headers)) { if (!detail::is_connection_persistent(req) && payload_max_length_ > 0 && payload_max_length_ < (std::numeric_limits::max)()) { auto has_data = strm.is_readable(); if (!has_data) { auto s = strm.socket(); if (s != INVALID_SOCKET) { has_data = detail::select_read(s, 0, 0) > 0; } } if (has_data) { auto result = detail::read_content_without_length(strm, payload_max_length_, out); if (result == detail::ReadContentResult::PayloadTooLarge) { res.status = StatusCode::PayloadTooLarge_413; return false; } else if (result != detail::ReadContentResult::Success) { return false; } return true; } } return true; } #else if (!req.has_header("Content-Length") && !detail::is_chunked_transfer_encoding(req.headers)) { return true; } #endif if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr, out, true)) { return false; } req.body_consumed_ = true; if (req.is_multipart_form_data()) { if (!multipart_form_data_parser.is_valid()) { res.status = StatusCode::BadRequest_400; output_error_log(Error::MultipartParsing, &req); return false; } } return true; } 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)) { std::string sub_path = "/" + req.path.substr(entry.mount_point.size()); if (detail::is_valid_path(sub_path)) { auto path = entry.base_dir + sub_path; if (path.back() == '/') { path += "index.html"; } // Defense-in-depth: is_valid_path blocks ".." traversal in the URL, // but symlinks/junctions can still escape the base directory. if (!entry.resolved_base_dir.empty()) { std::string resolved_path; if (detail::canonicalize_path(path.c_str(), resolved_path) && !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()) { res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301); return true; } if (stat.is_file()) { for (const auto &kv : entry.headers) { res.set_header(kv.first, kv.second); } auto etag = detail::compute_etag(stat); if (!etag.empty()) { res.set_header("ETag", etag); } auto mtime = stat.mtime(); auto last_modified = detail::file_mtime_to_http_date(mtime); if (!last_modified.empty()) { res.set_header("Last-Modified", last_modified); } if (check_if_not_modified(req, res, etag, mtime)) { return true; } check_if_range(req, etag, mtime); auto mm = std::make_shared(path.c_str()); if (!mm->is_open()) { output_error_log(Error::OpenFile, &req); return false; } res.set_content_provider( mm->size(), detail::find_content_type(path, file_extension_and_mimetype_map_, default_file_mimetype_), [mm](size_t offset, size_t length, DataSink &sink) -> bool { sink.write(mm->data() + offset, length); return true; }); if (req.method != "HEAD" && file_request_handler_) { file_request_handler_(req, res); } return true; } else { output_error_log(Error::OpenFile, &req); } } } } return false; } bool Server::check_if_not_modified(const Request &req, Response &res, const std::string &etag, time_t mtime) const { // Handle conditional GET: // 1. If-None-Match takes precedence (RFC 9110 Section 13.1.2) // 2. If-Modified-Since is checked only when If-None-Match is absent if (req.has_header("If-None-Match")) { if (!etag.empty()) { auto val = req.get_header_value("If-None-Match"); // NOTE: We use exact string matching here. This works correctly // because our server always generates weak ETags (W/"..."), and // clients typically send back the same ETag they received. // RFC 9110 Section 8.8.3.2 allows weak comparison for // If-None-Match, where W/"x" and "x" would match, but this // simplified implementation requires exact matches. auto ret = detail::split_find(val.data(), val.data() + val.size(), ',', [&](const char *b, const char *e) { auto seg_len = static_cast(e - b); return (seg_len == 1 && *b == '*') || (seg_len == etag.size() && std::equal(b, e, etag.begin())); }); if (ret) { res.status = StatusCode::NotModified_304; return true; } } } else if (req.has_header("If-Modified-Since")) { auto val = req.get_header_value("If-Modified-Since"); auto t = detail::parse_http_date(val); if (t != static_cast(-1) && mtime <= t) { res.status = StatusCode::NotModified_304; return true; } } return false; } bool Server::check_if_range(Request &req, const std::string &etag, time_t mtime) const { // Handle If-Range for partial content requests (RFC 9110 // Section 13.1.5). If-Range is only evaluated when Range header is // present. If the validator matches, serve partial content; otherwise // serve full content. if (!req.ranges.empty() && req.has_header("If-Range")) { auto val = req.get_header_value("If-Range"); auto is_valid_range = [&]() { if (detail::is_strong_etag(val)) { // RFC 9110 Section 13.1.5: If-Range requires strong ETag // comparison. return (!etag.empty() && val == etag); } else if (detail::is_weak_etag(val)) { // Weak ETags are not valid for If-Range (RFC 9110 Section 13.1.5) return false; } else { // HTTP-date comparison auto t = detail::parse_http_date(val); return (t != static_cast(-1) && mtime <= t); } }; if (!is_valid_range()) { // Validator doesn't match: ignore Range and serve full content req.ranges.clear(); return false; } } return true; } socket_t Server::create_server_socket(const std::string &host, int port, int socket_flags, SocketOptions socket_options) const { return detail::create_socket( host, std::string(), port, address_family_, socket_flags, tcp_nodelay_, ipv6_v6only_, std::move(socket_options), [&](socket_t sock, struct addrinfo &ai, bool & /*quit*/) -> bool { if (::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { output_error_log(Error::BindIPAddress, nullptr); return false; } if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) { output_error_log(Error::Listen, nullptr); return false; } return true; }); } int Server::bind_internal(const std::string &host, int port, int socket_flags) { if (is_decommissioned) { return -1; } if (!is_valid()) { return -1; } svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_); if (svr_sock_ == INVALID_SOCKET) { return -1; } if (port == 0) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); if (getsockname(svr_sock_, reinterpret_cast(&addr), &addr_len) == -1) { output_error_log(Error::GetSockName, nullptr); return -1; } if (addr.ss_family == AF_INET) { return ntohs(reinterpret_cast(&addr)->sin_port); } else if (addr.ss_family == AF_INET6) { return ntohs(reinterpret_cast(&addr)->sin6_port); } else { output_error_log(Error::UnsupportedAddressFamily, nullptr); return -1; } } else { return port; } } bool Server::listen_internal() { if (is_decommissioned) { return false; } auto ret = true; is_running_ = true; auto se = detail::scope_exit([&]() { is_running_ = false; }); if (start_handler_) { start_handler_(); } { std::unique_ptr task_queue(new_task_queue()); while (svr_sock_ != INVALID_SOCKET) { #ifndef _WIN32 if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) { #endif auto val = detail::select_read(svr_sock_, idle_interval_sec_, idle_interval_usec_); if (val == 0) { // Timeout task_queue->on_idle(); continue; } #ifndef _WIN32 } #endif #if defined _WIN32 // sockets connected via WASAccept inherit flags NO_HANDLE_INHERIT, // OVERLAPPED socket_t sock = WSAAccept(svr_sock_, nullptr, nullptr, nullptr, 0); #elif defined SOCK_CLOEXEC socket_t sock = accept4(svr_sock_, nullptr, nullptr, SOCK_CLOEXEC); #else socket_t sock = accept(svr_sock_, nullptr, nullptr); #endif if (sock == INVALID_SOCKET) { if (errno == EMFILE) { // The per-process limit of open file descriptors has been reached. // Try to accept new connections after a short sleep. std::this_thread::sleep_for(std::chrono::microseconds{1}); continue; } else if (errno == EINTR || errno == EAGAIN) { continue; } if (svr_sock_ != INVALID_SOCKET) { detail::close_socket(svr_sock_); ret = false; output_error_log(Error::Connection, nullptr); } else { ; // The server socket was closed by user. } break; } detail::set_socket_opt_time(sock, SOL_SOCKET, SO_RCVTIMEO, read_timeout_sec_, read_timeout_usec_); detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO, write_timeout_sec_, write_timeout_usec_); if (tcp_nodelay_) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); } if (!task_queue->enqueue( [this, sock]() { process_and_close_socket(sock); })) { output_error_log(Error::ResourceExhaustion, nullptr); detail::shutdown_socket(sock); detail::close_socket(sock); } } task_queue->shutdown(); } is_decommissioned = !ret; return ret; } bool Server::routing(Request &req, Response &res, Stream &strm) { if (pre_routing_handler_ && pre_routing_handler_(req, res) == HandlerResponse::Handled) { return true; } // File handler if ((req.method == "GET" || req.method == "HEAD") && handle_file_request(req, res)) { return true; } if (detail::expect_content(req)) { // Content reader handler { // Track whether the ContentReader was aborted due to the decompressed // payload exceeding `payload_max_length_`. // The user handler runs after the lambda returns, so we must restore the // 413 status if the handler overwrites it. bool content_reader_payload_too_large = false; ContentReader reader( [&](ContentReceiver receiver) { auto result = read_content_with_content_receiver( strm, req, res, std::move(receiver), nullptr, nullptr); if (!result) { output_error_log(Error::Read, &req); if (res.status == StatusCode::PayloadTooLarge_413) { content_reader_payload_too_large = true; } } return result; }, [&](FormDataHeader header, ContentReceiver receiver) { auto result = read_content_with_content_receiver( strm, req, res, nullptr, std::move(header), std::move(receiver)); if (!result) { output_error_log(Error::Read, &req); if (res.status == StatusCode::PayloadTooLarge_413) { content_reader_payload_too_large = true; } } return result; }); bool dispatched = false; if (req.method == "POST") { dispatched = dispatch_request_for_content_reader( req, res, std::move(reader), post_handlers_for_content_reader_); } else if (req.method == "PUT") { dispatched = dispatch_request_for_content_reader( req, res, std::move(reader), put_handlers_for_content_reader_); } else if (req.method == "PATCH") { dispatched = dispatch_request_for_content_reader( req, res, std::move(reader), patch_handlers_for_content_reader_); } else if (req.method == "DELETE") { dispatched = dispatch_request_for_content_reader( req, res, std::move(reader), delete_handlers_for_content_reader_); } if (dispatched) { if (content_reader_payload_too_large) { // Enforce the limit: override any status the handler may have set // and return false so the error path sends a plain 413 response. res.status = StatusCode::PayloadTooLarge_413; res.body.clear(); res.content_length_ = 0; res.content_provider_ = nullptr; return false; } return true; } } // NOTE: `req.body` is not read here. For a regular handler the body is // read inside dispatch_request(), after the route has matched and the // pre-request handler has approved the request, so that a rejected // request (e.g. failed authentication) never forces us to buffer a // potentially large body. } // Regular handler if (req.method == "GET" || req.method == "HEAD") { return dispatch_request(req, res, get_handlers_, strm); } else if (req.method == "POST") { return dispatch_request(req, res, post_handlers_, strm); } else if (req.method == "PUT") { return dispatch_request(req, res, put_handlers_, strm); } else if (req.method == "DELETE") { return dispatch_request(req, res, delete_handlers_, strm); } else if (req.method == "OPTIONS") { return dispatch_request(req, res, options_handlers_, strm); } else if (req.method == "PATCH") { return dispatch_request(req, res, patch_handlers_, strm); } res.status = StatusCode::BadRequest_400; return false; } bool Server::dispatch_request(Request &req, Response &res, const Handlers &handlers, Stream &strm) { for (const auto &x : handlers) { const auto &matcher = x.first; const auto &handler = x.second; if (matcher->match(req)) { req.matched_route = matcher->pattern(); // Run the pre-request handler before reading the body so a rejected // request (e.g. failed authentication) never forces us to buffer a // potentially large body. `req.matched_route` is available here. if (pre_request_handler_ && pre_request_handler_(req, res) == HandlerResponse::Handled) { return true; } // The route matched and the request was approved; read the body now. if (detail::expect_content(req) && !read_content(strm, req, res)) { output_error_log(Error::Read, &req); return false; } handler(req, res); return true; } } return false; } void Server::apply_ranges(const Request &req, Response &res, std::string &content_type, std::string &boundary) const { if (req.ranges.size() > 1 && res.status == StatusCode::PartialContent_206) { auto it = res.headers.find("Content-Type"); if (it != res.headers.end()) { content_type = it->second; res.headers.erase(it); } boundary = detail::make_multipart_data_boundary(); res.set_header("Content-Type", "multipart/byteranges; boundary=" + boundary); } auto type = detail::encoding_type(req, res); if (res.body.empty()) { if (res.content_length_ > 0) { size_t length = 0; if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) { length = res.content_length_; } else if (req.ranges.size() == 1) { auto offset_and_length = detail::get_range_offset_and_length( req.ranges[0], res.content_length_); length = offset_and_length.second; auto content_range = detail::make_content_range_header_field( offset_and_length, res.content_length_); res.set_header("Content-Range", content_range); } else { length = detail::get_multipart_ranges_data_length( req, boundary, content_type, res.content_length_); } res.set_header("Content-Length", std::to_string(length)); } else { if (res.content_provider_) { if (res.is_chunked_content_provider_) { res.set_header("Transfer-Encoding", "chunked"); if (type != detail::EncodingType::None) { res.set_header("Content-Encoding", detail::encoding_name(type)); res.set_header("Vary", "Accept-Encoding"); } } } } } else { if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) { ; } else if (req.ranges.size() == 1) { auto offset_and_length = detail::get_range_offset_and_length(req.ranges[0], res.body.size()); auto offset = offset_and_length.first; auto length = offset_and_length.second; auto content_range = detail::make_content_range_header_field( offset_and_length, res.body.size()); res.set_header("Content-Range", content_range); assert(offset + length <= res.body.size()); res.body = res.body.substr(offset, length); } else { std::string data; detail::make_multipart_ranges_data(req, res, boundary, content_type, res.body.size(), data); res.body.swap(data); } if (type != detail::EncodingType::None) { output_pre_compression_log(req, res); if (auto compressor = detail::make_compressor(type)) { std::string compressed; if (compressor->compress(res.body.data(), res.body.size(), true, [&](const char *data, size_t data_len) { compressed.append(data, data_len); return true; })) { res.body.swap(compressed); res.set_header("Content-Encoding", detail::encoding_name(type)); res.set_header("Vary", "Accept-Encoding"); } } } auto length = std::to_string(res.body.size()); res.set_header("Content-Length", length); } } bool Server::dispatch_request_for_content_reader( Request &req, Response &res, ContentReader content_reader, const HandlersForContentReader &handlers) const { for (const auto &x : handlers) { const auto &matcher = x.first; const auto &handler = x.second; if (matcher->match(req)) { req.matched_route = matcher->pattern(); if (!pre_request_handler_ || pre_request_handler_(req, res) != HandlerResponse::Handled) { handler(req, res, content_reader); } return true; } } return false; } std::string get_client_ip(const std::string &x_forwarded_for, const std::vector &trusted_proxies) { // X-Forwarded-For is a comma-separated list per RFC 7239 std::vector ip_list; detail::split(x_forwarded_for.data(), x_forwarded_for.data() + x_forwarded_for.size(), ',', [&](const char *b, const char *e) { auto r = detail::trim(b, e, 0, static_cast(e - b)); ip_list.emplace_back(std::string(b + r.first, b + r.second)); }); // A malformed X-Forwarded-For (empty, comma-only, whitespace-only) yields // no segments. Signal "no client IP derived" with an empty string so the // caller can fall back to the connection-level remote address. if (ip_list.empty()) { return std::string(); } for (size_t i = 0; i < ip_list.size(); ++i) { auto ip = ip_list[i]; auto is_trusted_proxy = std::any_of(trusted_proxies.begin(), trusted_proxies.end(), [&](const std::string &proxy) { return ip == proxy; }); if (is_trusted_proxy) { if (i == 0) { // If the trusted proxy is the first IP, there's no preceding client IP return ip; } else { // Return the IP immediately before the trusted proxy return ip_list[i - 1]; } } } // If no trusted proxy is found, return the first IP in the list return ip_list.front(); } bool Server::process_request(Stream &strm, const std::string &remote_addr, int remote_port, const std::string &local_addr, int local_port, bool close_connection, bool &connection_closed, const std::function &setup_request, bool *websocket_upgraded) { std::array buf{}; detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); // Connection has been closed on client if (!line_reader.getline()) { return false; } Request req; req.start_time_ = std::chrono::steady_clock::now(); req.remote_addr = remote_addr; req.remote_port = remote_port; req.local_addr = local_addr; req.local_port = local_port; Response res; res.version = "HTTP/1.1"; res.headers = default_headers_; // Request line and headers if (!parse_request_line(line_reader.ptr(), req)) { res.status = StatusCode::BadRequest_400; output_error_log(Error::InvalidRequestLine, &req); return write_response(strm, close_connection, req, res); } // Request headers if (!detail::read_headers(strm, req.headers)) { res.status = StatusCode::BadRequest_400; output_error_log(Error::InvalidHeaders, &req); return write_response(strm, close_connection, req, res); } // RFC 9112 ยง6.3: Reject requests with both a non-zero Content-Length and // any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is // tolerated for compatibility with existing clients. if (req.get_header_value_u64("Content-Length") > 0 && req.has_header("Transfer-Encoding")) { connection_closed = true; res.status = StatusCode::BadRequest_400; return write_response(strm, close_connection, req, res); } // Check if the request URI doesn't exceed the limit if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { connection_closed = true; res.status = StatusCode::UriTooLong_414; output_error_log(Error::ExceedUriMaxLength, &req); return write_response(strm, close_connection, req, res); } if (req.get_header_value("Connection") == "close") { connection_closed = true; } if (req.version == "HTTP/1.0" && req.get_header_value("Connection") != "Keep-Alive") { connection_closed = true; } if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) { auto x_forwarded_for = req.get_header_value("X-Forwarded-For"); auto derived = get_client_ip(x_forwarded_for, trusted_proxies_); req.remote_addr = derived.empty() ? remote_addr : derived; } else { req.remote_addr = remote_addr; } req.remote_port = remote_port; req.local_addr = local_addr; req.local_port = local_port; if (req.has_header("Accept")) { const auto &accept_header = req.get_header_value("Accept"); if (!detail::parse_accept_header(accept_header, req.accept_content_types)) { connection_closed = true; res.status = StatusCode::BadRequest_400; output_error_log(Error::HTTPParsing, &req); return write_response(strm, close_connection, req, res); } } if (req.has_header("Range")) { const auto &range_header_value = req.get_header_value("Range"); if (!detail::parse_range_header(range_header_value, req.ranges)) { connection_closed = true; res.status = StatusCode::RangeNotSatisfiable_416; output_error_log(Error::InvalidRangeHeader, &req); return write_response(strm, close_connection, req, res); } } if (setup_request) { setup_request(req); } if (req.get_header_value("Expect") == "100-continue") { int status = StatusCode::Continue_100; if (expect_100_continue_handler_) { status = expect_100_continue_handler_(req, res); } switch (status) { case StatusCode::Continue_100: case StatusCode::ExpectationFailed_417: detail::write_response_line(strm, status); strm.write("\r\n"); break; default: connection_closed = true; return write_response(strm, true, req, res); } } // Setup `is_connection_closed` method auto sock = strm.socket(); req.is_connection_closed = [sock]() { return !detail::is_socket_alive(sock); }; // WebSocket upgrade // Check pre_routing_handler_ before upgrading so that authentication // and other middleware can reject the request with an HTTP response // (e.g., 401) before the protocol switches. if (detail::is_websocket_upgrade(req)) { if (pre_routing_handler_ && pre_routing_handler_(req, res) == HandlerResponse::Handled) { if (res.status == -1) { res.status = StatusCode::OK_200; } return write_response(strm, close_connection, req, res); } // Find matching WebSocket handler for (const auto &entry : websocket_handlers_) { if (entry.matcher->match(req)) { // Compute accept key auto client_key = req.get_header_value("Sec-WebSocket-Key"); auto accept_key = detail::websocket_accept_key(client_key); // Negotiate subprotocol std::string selected_subprotocol; if (entry.sub_protocol_selector) { auto protocol_header = req.get_header_value("Sec-WebSocket-Protocol"); if (!protocol_header.empty()) { std::vector protocols; std::istringstream iss(protocol_header); std::string token; while (std::getline(iss, token, ',')) { // Trim whitespace auto start = token.find_first_not_of(' '); auto end = token.find_last_not_of(' '); if (start != std::string::npos) { protocols.push_back(token.substr(start, end - start + 1)); } } selected_subprotocol = entry.sub_protocol_selector(protocols); } } // Send 101 Switching Protocols std::string handshake_response = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: " + accept_key + "\r\n"; if (!selected_subprotocol.empty()) { if (!detail::fields::is_field_value(selected_subprotocol)) { return false; } handshake_response += "Sec-WebSocket-Protocol: " + selected_subprotocol + "\r\n"; } handshake_response += "\r\n"; if (strm.write(handshake_response.data(), handshake_response.size()) < 0) { return false; } connection_closed = true; if (websocket_upgraded) { *websocket_upgraded = true; } { // Use WebSocket-specific read timeout instead of HTTP timeout strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0); ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_, websocket_max_missed_pongs_); entry.handler(req, ws); } return true; } } // No matching handler - fall through to 404 } // Routing auto routed = false; #ifdef CPPHTTPLIB_NO_EXCEPTIONS routed = routing(req, res, strm); #else try { routed = routing(req, res, strm); } catch (std::exception &) { if (exception_handler_) { auto ep = std::current_exception(); exception_handler_(req, res, ep); routed = true; } else { res.status = StatusCode::InternalServerError_500; } } catch (...) { if (exception_handler_) { auto ep = std::current_exception(); exception_handler_(req, res, ep); routed = true; } else { res.status = StatusCode::InternalServerError_500; } } #endif auto ret = false; if (routed) { if (res.status == -1) { res.status = req.ranges.empty() ? StatusCode::OK_200 : StatusCode::PartialContent_206; } // Serve file content by using a content provider auto file_open_error = false; if (!res.file_content_path_.empty()) { const auto &path = res.file_content_path_; auto mm = std::make_shared(path.c_str()); if (!mm->is_open()) { res.body.clear(); res.content_length_ = 0; res.content_provider_ = nullptr; res.status = StatusCode::NotFound_404; output_error_log(Error::OpenFile, &req); file_open_error = true; } else { auto content_type = res.file_content_content_type_; if (content_type.empty()) { content_type = detail::find_content_type( path, file_extension_and_mimetype_map_, default_file_mimetype_); } res.set_content_provider( mm->size(), content_type, [mm](size_t offset, size_t length, DataSink &sink) -> bool { sink.write(mm->data() + offset, length); return true; }); } } if (file_open_error) { ret = write_response(strm, close_connection, req, res); } else if (detail::range_error(req, res)) { res.body.clear(); res.content_length_ = 0; res.content_provider_ = nullptr; res.status = StatusCode::RangeNotSatisfiable_416; ret = write_response(strm, close_connection, req, res); } else { ret = write_response_with_content(strm, close_connection, req, res); } } else { if (res.status == -1) { res.status = StatusCode::NotFound_404; } ret = write_response(strm, close_connection, req, res); } // Drain any unconsumed framed body to prevent request smuggling on // keep-alive. Without framing there is no body to drain โ€” reading would // consume the next request (issue #2450). if (!req.body_consumed_ && detail::has_framed_body(req)) { int dummy_status; if (!detail::read_content( strm, req, payload_max_length_, dummy_status, nullptr, [](const char *, size_t, size_t, size_t) { return true; }, false)) { connection_closed = true; } } return ret; } bool Server::is_valid() const { return true; } bool Server::process_and_close_socket(socket_t sock) { std::string remote_addr; int remote_port = 0; detail::get_remote_ip_and_port(sock, remote_addr, remote_port); std::string local_addr; int local_port = 0; detail::get_local_ip_and_port(sock, local_addr, local_port); bool websocket_upgraded = false; auto ret = detail::process_server_socket( svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, [&](Stream &strm, bool close_connection, bool &connection_closed) { return process_request(strm, remote_addr, remote_port, local_addr, local_port, close_connection, connection_closed, nullptr, &websocket_upgraded); }); detail::shutdown_socket(sock); detail::close_socket(sock); return ret; } void Server::output_log(const Request &req, const Response &res) const { if (logger_) { std::lock_guard guard(logger_mutex_); logger_(req, res); } } void Server::output_pre_compression_log(const Request &req, const Response &res) const { if (pre_compression_logger_) { std::lock_guard guard(logger_mutex_); pre_compression_logger_(req, res); } } void Server::output_error_log(const Error &err, const Request *req) const { if (error_logger_) { std::lock_guard guard(logger_mutex_); error_logger_(err, req); } } /* * Group 5: ClientImpl and Client (Universal) implementation */ // HTTP client implementation ClientImpl::ClientImpl(const std::string &host) : ClientImpl(host, 80, std::string(), std::string()) {} ClientImpl::ClientImpl(const std::string &host, int port) : ClientImpl(host, port, std::string(), std::string()) {} ClientImpl::ClientImpl(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path) : host_(detail::escape_abstract_namespace_unix_domain(host)), port_(port), client_cert_path_(client_cert_path), client_key_path_(client_key_path) {} ClientImpl::~ClientImpl() { // Wait until all the requests in flight are handled. size_t retry_count = 10; while (retry_count-- > 0) { { std::lock_guard guard(socket_mutex_); if (socket_requests_in_flight_ == 0) { break; } } std::this_thread::sleep_for(std::chrono::milliseconds{1}); } std::lock_guard guard(socket_mutex_); shutdown_socket(socket_); close_socket(socket_); } bool ClientImpl::is_valid() const { return true; } void ClientImpl::copy_settings(const ClientImpl &rhs) { client_cert_path_ = rhs.client_cert_path_; client_key_path_ = rhs.client_key_path_; connection_timeout_sec_ = rhs.connection_timeout_sec_; read_timeout_sec_ = rhs.read_timeout_sec_; read_timeout_usec_ = rhs.read_timeout_usec_; write_timeout_sec_ = rhs.write_timeout_sec_; write_timeout_usec_ = rhs.write_timeout_usec_; max_timeout_msec_ = rhs.max_timeout_msec_; basic_auth_username_ = rhs.basic_auth_username_; basic_auth_password_ = rhs.basic_auth_password_; bearer_token_auth_token_ = rhs.bearer_token_auth_token_; keep_alive_ = rhs.keep_alive_; follow_location_ = rhs.follow_location_; path_encode_ = rhs.path_encode_; address_family_ = rhs.address_family_; tcp_nodelay_ = rhs.tcp_nodelay_; ipv6_v6only_ = rhs.ipv6_v6only_; socket_options_ = rhs.socket_options_; compress_ = rhs.compress_; decompress_ = rhs.decompress_; payload_max_length_ = rhs.payload_max_length_; has_payload_max_length_ = rhs.has_payload_max_length_; interface_ = rhs.interface_; proxy_host_ = rhs.proxy_host_; proxy_port_ = rhs.proxy_port_; proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_; proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_; proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_; no_proxy_entries_ = rhs.no_proxy_entries_; logger_ = rhs.logger_; error_logger_ = rhs.error_logger_; #ifdef CPPHTTPLIB_SSL_ENABLED digest_auth_username_ = rhs.digest_auth_username_; digest_auth_password_ = rhs.digest_auth_password_; proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_; proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_; ca_cert_file_path_ = rhs.ca_cert_file_path_; ca_cert_dir_path_ = rhs.ca_cert_dir_path_; server_certificate_verification_ = rhs.server_certificate_verification_; server_hostname_verification_ = rhs.server_hostname_verification_; system_ca_mode_ = rhs.system_ca_mode_; #endif } bool ClientImpl::is_proxy_enabled_for_host(const std::string &host) const { if (proxy_host_.empty() || proxy_port_ == -1) { return false; } if (no_proxy_entries_.empty()) { return true; } // host_ is const so its normalized form is invariant; cache it. The // cross-host path (setup_redirect_client passing next_host) re-normalizes. if (host == host_) { if (!host_normalized_valid_) { host_normalized_ = detail::normalize_target(host_); host_normalized_valid_ = true; } return !detail::host_matches_no_proxy(host_normalized_, no_proxy_entries_); } auto target = detail::normalize_target(host); return !detail::host_matches_no_proxy(target, no_proxy_entries_); } socket_t ClientImpl::create_client_socket(Error &error) const { if (is_proxy_enabled_for_host(host_)) { return detail::create_client_socket( proxy_host_, std::string(), proxy_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); } // Check is custom IP specified for host_ std::string ip; auto it = addr_map_.find(host_); if (it != addr_map_.end()) { ip = it->second; } return detail::create_client_socket( 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); } bool ClientImpl::create_and_connect_socket(Socket &socket, Error &error) { auto sock = create_client_socket(error); if (sock == INVALID_SOCKET) { return false; } socket.sock = sock; return true; } bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) { return create_and_connect_socket(socket, error); } bool ClientImpl::setup_proxy_connection( Socket & /*socket*/, std::chrono::time_point /*start_time*/, Response & /*res*/, bool & /*success*/, Error & /*error*/) { return true; } void ClientImpl::shutdown_ssl(Socket & /*socket*/, bool /*shutdown_gracefully*/) { // If there are any requests in flight from threads other than us, then it's // a thread-unsafe race because individual ssl* objects are not thread-safe. assert(socket_requests_in_flight_ == 0 || socket_requests_are_from_thread_ == std::this_thread::get_id()); } void ClientImpl::shutdown_socket(Socket &socket) const { if (socket.sock == INVALID_SOCKET) { return; } detail::shutdown_socket(socket.sock); } void ClientImpl::close_socket(Socket &socket) { // If there are requests in flight in another thread, usually closing // the socket will be fine and they will simply receive an error when // using the closed socket, but it is still a bug since rarely the OS // may reassign the socket id to be used for a new socket, and then // suddenly they will be operating on a live socket that is different // than the one they intended! assert(socket_requests_in_flight_ == 0 || socket_requests_are_from_thread_ == std::this_thread::get_id()); // It is also a bug if this happens while SSL is still active #ifdef CPPHTTPLIB_SSL_ENABLED assert(socket.ssl == nullptr); #endif if (socket.sock == INVALID_SOCKET) { return; } detail::close_socket(socket.sock); socket.sock = INVALID_SOCKET; } void ClientImpl::disconnect(bool gracefully) { shutdown_ssl(socket_, gracefully); shutdown_socket(socket_); close_socket(socket_); } bool ClientImpl::read_response_line(Stream &strm, const Request &req, Response &res, bool skip_100_continue) const { std::array buf{}; detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); 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)) { 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]); } return true; } bool ClientImpl::send(Request &req, Response &res, Error &error) { std::lock_guard request_mutex_guard(request_mutex_); auto ret = send_(req, res, error); if (error == Error::SSLPeerCouldBeClosed_) { assert(!ret); ret = send_(req, res, error); // If still failing with SSLPeerCouldBeClosed_, convert to Read error if (error == Error::SSLPeerCouldBeClosed_) { error = Error::Read; } } return ret; } bool ClientImpl::send_(Request &req, Response &res, Error &error) { { std::lock_guard guard(socket_mutex_); // Set this to false immediately - if it ever gets set to true by the end // of the request, we know another thread instructed us to close the // socket. socket_should_be_closed_when_request_is_done_ = false; auto is_alive = false; if (socket_.is_open()) { is_alive = detail::is_socket_alive(socket_.sock); #ifdef CPPHTTPLIB_SSL_ENABLED if (is_alive && is_ssl()) { if (tls::is_peer_closed(socket_.ssl, socket_.sock)) { is_alive = false; } } #endif if (!is_alive) { // Peer seems gone โ€” non-graceful shutdown to avoid SIGPIPE. disconnect(/*gracefully=*/false); } } if (!is_alive) { if (!ensure_socket_connection(socket_, error)) { output_error_log(error, &req); return false; } { auto success = true; if (!setup_proxy_connection(socket_, req.start_time_, res, success, error)) { if (!success) { output_error_log(error, &req); } return success; } } } // Mark the current socket as being in use so that it cannot be closed by // anyone else while this request is ongoing, even though we will be // releasing the mutex. if (socket_requests_in_flight_ > 1) { assert(socket_requests_are_from_thread_ == std::this_thread::get_id()); } socket_requests_in_flight_ += 1; socket_requests_are_from_thread_ = std::this_thread::get_id(); } for (const auto &header : default_headers_) { if (req.headers.find(header.first) == req.headers.end()) { req.headers.insert(header); } } auto ret = false; auto close_connection = !keep_alive_; auto se = detail::scope_exit([&]() { // Briefly lock mutex in order to mark that a request is no longer ongoing std::lock_guard guard(socket_mutex_); socket_requests_in_flight_ -= 1; if (socket_requests_in_flight_ <= 0) { assert(socket_requests_in_flight_ == 0); socket_requests_are_from_thread_ = std::thread::id(); } if (socket_should_be_closed_when_request_is_done_ || close_connection || !ret) { disconnect(/*gracefully=*/true); } }); ret = process_socket(socket_, req.start_time_, [&](Stream &strm) { return handle_request(strm, req, res, close_connection, error); }); if (!ret) { if (error == Error::Success) { error = Error::Unknown; output_error_log(error, &req); } } return ret; } Result ClientImpl::send(const Request &req) { auto req2 = req; return send_(std::move(req2)); } Result ClientImpl::send_(Request &&req) { auto res = detail::make_unique(); auto error = Error::Success; auto ret = send(req, *res, error); #ifdef CPPHTTPLIB_SSL_ENABLED return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers), last_ssl_error_, last_backend_error_}; #else return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)}; #endif } void ClientImpl::prepare_default_headers(Request &r, bool for_stream, const std::string &ct) { (void)for_stream; for (const auto &header : default_headers_) { if (!r.has_header(header.first)) { r.headers.insert(header); } } if (!r.has_header("Host")) { if (address_family_ == AF_UNIX) { r.headers.emplace("Host", "localhost"); } else { r.headers.emplace( "Host", detail::make_host_and_port_string(host_, port_, is_ssl())); } } if (!r.has_header("Accept")) { r.headers.emplace("Accept", "*/*"); } if (!r.content_receiver) { if (!r.has_header("Accept-Encoding")) { std::string accept_encoding; #ifdef CPPHTTPLIB_BROTLI_SUPPORT accept_encoding = "br"; #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (!accept_encoding.empty()) { accept_encoding += ", "; } accept_encoding += "gzip, deflate"; #endif #ifdef CPPHTTPLIB_ZSTD_SUPPORT if (!accept_encoding.empty()) { accept_encoding += ", "; } accept_encoding += "zstd"; #endif 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 } if (!r.body.empty()) { if (!ct.empty() && !r.has_header("Content-Type")) { r.headers.emplace("Content-Type", ct); } if (!r.has_header("Content-Length")) { r.headers.emplace("Content-Length", std::to_string(r.body.size())); } } } ClientImpl::StreamHandle ClientImpl::open_stream(const std::string &method, const std::string &path, const Params ¶ms, const Headers &headers, const std::string &body, const std::string &content_type) { StreamHandle handle; handle.response = detail::make_unique(); handle.error = Error::Success; auto query_path = params.empty() ? path : append_query_params(path, params); handle.connection_ = detail::make_unique(); { std::lock_guard guard(socket_mutex_); auto is_alive = false; if (socket_.is_open()) { is_alive = detail::is_socket_alive(socket_.sock); #ifdef CPPHTTPLIB_SSL_ENABLED if (is_alive && is_ssl()) { if (tls::is_peer_closed(socket_.ssl, socket_.sock)) { is_alive = false; } } #endif if (!is_alive) { disconnect(/*gracefully=*/false); } } if (!is_alive) { if (!ensure_socket_connection(socket_, handle.error)) { handle.response.reset(); return handle; } { auto success = true; auto start_time = std::chrono::steady_clock::now(); if (!setup_proxy_connection(socket_, start_time, *handle.response, success, handle.error)) { if (!success) { handle.response.reset(); } return handle; } } } transfer_socket_ownership_to_handle(handle); } #ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl() && handle.connection_->session) { handle.socket_stream_ = detail::make_unique( handle.connection_->sock, handle.connection_->session, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_); } else { handle.socket_stream_ = detail::make_unique( handle.connection_->sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_); } #else handle.socket_stream_ = detail::make_unique( handle.connection_->sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_); #endif handle.stream_ = handle.socket_stream_.get(); Request req; req.method = method; req.path = query_path; req.headers = headers; req.body = body; prepare_default_headers(req, true, content_type); auto &strm = *handle.stream_; if (detail::write_request_line(strm, req.method, req.path) < 0) { handle.error = Error::Write; handle.response.reset(); return handle; } if (!detail::check_and_write_headers(strm, req.headers, header_writer_, handle.error)) { handle.response.reset(); return handle; } if (!body.empty()) { if (strm.write(body.data(), body.size()) < 0) { handle.error = Error::Write; handle.response.reset(); return handle; } } if (!read_response_line(strm, req, *handle.response) || !detail::read_headers(strm, 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( handle.response->headers, "Content-Length", 0, 0, is_invalid); if (is_invalid) { handle.error = Error::Read; handle.response.reset(); return handle; } handle.body_reader_.has_content_length = true; handle.body_reader_.content_length = content_length; } auto transfer_encoding = handle.response->get_header_value("Transfer-Encoding"); handle.body_reader_.chunked = (transfer_encoding == "chunked"); auto content_encoding = handle.response->get_header_value("Content-Encoding"); if (!content_encoding.empty()) { handle.decompressor_ = detail::create_decompressor(content_encoding); } return handle; } ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) { if (!is_valid() || !response) { return -1; } if (decompressor_) { return read_with_decompression(buf, len); } auto n = detail::read_body_content(stream_, body_reader_, buf, len); if (n <= 0 && body_reader_.chunked && !trailers_parsed_ && stream_) { trailers_parsed_ = true; if (body_reader_.chunked_decoder) { if (!body_reader_.chunked_decoder->parse_trailers_into( response->trailers, response->headers)) { return n; } } else { detail::ChunkedDecoder dec(*stream_); if (!dec.parse_trailers_into(response->trailers, response->headers)) { return n; } } } return n; } ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf, size_t len) { if (decompress_offset_ < decompress_buffer_.size()) { auto available = decompress_buffer_.size() - decompress_offset_; auto to_copy = (std::min)(len, available); std::memcpy(buf, decompress_buffer_.data() + decompress_offset_, to_copy); decompress_offset_ += to_copy; decompressed_bytes_read_ += to_copy; return static_cast(to_copy); } decompress_buffer_.clear(); decompress_offset_ = 0; constexpr size_t kDecompressionBufferSize = 8192; char compressed_buf[kDecompressionBufferSize]; while (true) { auto n = detail::read_body_content(stream_, body_reader_, compressed_buf, sizeof(compressed_buf)); if (n <= 0) { return n; } bool decompress_ok = decompressor_->decompress( compressed_buf, static_cast(n), [this](const char *data, size_t data_len) { decompress_buffer_.append(data, data_len); auto limit = body_reader_.payload_max_length; if (decompressed_bytes_read_ + decompress_buffer_.size() > limit) { return false; } return true; }); if (!decompress_ok) { body_reader_.last_error = Error::Read; return -1; } if (!decompress_buffer_.empty()) { break; } } auto to_copy = (std::min)(len, decompress_buffer_.size()); std::memcpy(buf, decompress_buffer_.data(), to_copy); decompress_offset_ = to_copy; decompressed_bytes_read_ += to_copy; return static_cast(to_copy); } void ClientImpl::StreamHandle::parse_trailers_if_needed() { if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) { return; } trailers_parsed_ = true; const auto bufsiz = 128; char line_buf[bufsiz]; detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz); if (!line_reader.getline()) { return; } if (!detail::parse_trailers(line_reader, response->trailers, response->headers)) { return; } } namespace detail { ChunkedDecoder::ChunkedDecoder(Stream &s) : strm(s) {} ssize_t ChunkedDecoder::read_payload(char *buf, size_t len, size_t &out_chunk_offset, size_t &out_chunk_total) { if (finished) { return 0; } if (chunk_remaining == 0) { stream_line_reader lr(strm, line_buf, sizeof(line_buf)); if (!lr.getline()) { return -1; } // RFC 9112 ยง7.1: chunk-size = 1*HEXDIG const char *p = lr.ptr(); int v = 0; if (!is_hex(*p, v)) { return -1; } size_t chunk_len = 0; constexpr size_t chunk_len_max = (std::numeric_limits::max)(); for (; is_hex(*p, v); ++p) { if (chunk_len > (chunk_len_max >> 4)) { return -1; } chunk_len = (chunk_len << 4) | static_cast(v); } while (is_space_or_tab(*p)) { ++p; } if (*p != '\0' && *p != ';' && *p != '\r' && *p != '\n') { return -1; } if (chunk_len == 0) { chunk_remaining = 0; finished = true; out_chunk_offset = 0; out_chunk_total = 0; return 0; } chunk_remaining = chunk_len; last_chunk_total = chunk_remaining; last_chunk_offset = 0; } auto to_read = (std::min)(chunk_remaining, len); auto n = strm.read(buf, to_read); if (n <= 0) { return -1; } auto offset_before = last_chunk_offset; last_chunk_offset += static_cast(n); chunk_remaining -= static_cast(n); out_chunk_offset = offset_before; out_chunk_total = last_chunk_total; if (chunk_remaining == 0) { stream_line_reader lr(strm, line_buf, sizeof(line_buf)); if (!lr.getline()) { return -1; } if (std::strcmp(lr.ptr(), "\r\n") != 0) { return -1; } } return n; } bool ChunkedDecoder::parse_trailers_into(Headers &dest, const Headers &src_headers) { stream_line_reader lr(strm, line_buf, sizeof(line_buf)); if (!lr.getline()) { return false; } return parse_trailers(lr, dest, src_headers); } } // namespace detail void ClientImpl::transfer_socket_ownership_to_handle(StreamHandle &handle) { handle.connection_->sock = socket_.sock; #ifdef CPPHTTPLIB_SSL_ENABLED handle.connection_->session = socket_.ssl; socket_.ssl = nullptr; #endif socket_.sock = INVALID_SOCKET; } bool ClientImpl::handle_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error) { if (req.path.empty()) { error = Error::Connection; output_error_log(error, &req); return false; } auto req_save = req; bool ret; if (!is_ssl() && is_proxy_enabled_for_host(host_)) { auto req2 = req; req2.path = "http://" + detail::make_host_and_port_string(host_, port_, false) + req.path; ret = process_request(strm, req2, res, close_connection, error); req = std::move(req2); req.path = req_save.path; } else { ret = process_request(strm, req, res, close_connection, error); } if (!ret) { return false; } if (res.get_header_value("Connection") == "close" || (res.version == "HTTP/1.0" && res.reason != "Connection established")) { // NOTE: this requires a not-entirely-obvious chain of calls to be correct // for this to be safe. // This is safe to call because handle_request is only called by send_ // which locks the request mutex during the process. It would be a bug // to call it from a different thread since it's a thread-safety issue // to do these things to the socket if another thread is using the socket. std::lock_guard guard(socket_mutex_); disconnect(/*gracefully=*/true); } if (300 < res.status && res.status < 400 && follow_location_) { req = std::move(req_save); ret = redirect(req, res, error); } #ifdef CPPHTTPLIB_SSL_ENABLED if ((res.status == StatusCode::Unauthorized_401 || res.status == StatusCode::ProxyAuthenticationRequired_407) && req.authorization_count_ < 5) { auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407; // Only retry when the 407 actually came from a proxy hop: plain HTTP // through an enabled proxy. HTTPS via CONNECT tunnels the 407 from the // origin (#2457); direct/bypassed origins have no proxy hop at all. if (is_proxy && !(!is_ssl() && is_proxy_enabled_for_host(host_))) { return ret; } const auto &username = is_proxy ? proxy_digest_auth_username_ : digest_auth_username_; const auto &password = is_proxy ? proxy_digest_auth_password_ : digest_auth_password_; if (!username.empty() && !password.empty()) { std::map auth; if (detail::parse_www_authenticate(res, auth, is_proxy)) { Request new_req = req; new_req.authorization_count_ += 1; new_req.headers.erase(is_proxy ? "Proxy-Authorization" : "Authorization"); new_req.headers.insert(detail::make_digest_authentication_header( req, auth, new_req.authorization_count_, detail::random_string(10), username, password, is_proxy)); Response new_res; ret = send(new_req, new_res, error); if (ret) { res = std::move(new_res); } } } } #endif return ret; } bool ClientImpl::redirect(Request &req, Response &res, Error &error) { if (req.redirect_count_ == 0) { error = Error::ExceedRedirectCount; output_error_log(error, &req); return false; } auto location = res.get_header_value("location"); if (location.empty()) { return false; } detail::UrlComponents uc; if (!detail::parse_url(location, uc)) { return false; } // Only follow http/https redirects if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") { return false; } auto scheme = is_ssl() ? "https" : "http"; auto next_scheme = std::move(uc.scheme); auto next_host = std::move(uc.host); auto port_str = std::move(uc.port); auto next_path = std::move(uc.path); auto next_query = std::move(uc.query); auto next_port = port_; if (!port_str.empty()) { if (!detail::parse_port(port_str, next_port)) { return false; } } else if (!next_scheme.empty()) { next_port = next_scheme == "https" ? 443 : 80; } if (next_scheme.empty()) { next_scheme = scheme; } if (next_host.empty()) { next_host = host_; } if (next_path.empty()) { next_path = "/"; } auto path = decode_path_component(next_path) + next_query; // Same host redirect - use current client if (next_scheme == scheme && next_host == host_ && next_port == port_) { return detail::redirect(*this, req, res, path, location, error); } // Cross-host/scheme redirect - create new client with robust setup return create_redirect_client(next_scheme, next_host, next_port, req, res, path, location, error); } // New method for robust redirect client creation bool ClientImpl::create_redirect_client( const std::string &scheme, const std::string &host, int port, Request &req, Response &res, const std::string &path, const std::string &location, Error &error) { // Determine if we need SSL auto need_ssl = (scheme == "https"); // Clean up request headers that are host/client specific // Remove headers that should not be carried over to new host auto headers_to_remove = std::vector{"Host", "Proxy-Authorization", "Authorization"}; for (const auto &header_name : headers_to_remove) { auto it = req.headers.find(header_name); while (it != req.headers.end()) { it = req.headers.erase(it); it = req.headers.find(header_name); } } // Create appropriate client type and handle redirect if (need_ssl) { #ifdef CPPHTTPLIB_SSL_ENABLED // Create SSL client for HTTPS redirect SSLClient redirect_client(host, port); // Setup basic client configuration first setup_redirect_client(redirect_client); redirect_client.enable_server_certificate_verification( server_certificate_verification_); redirect_client.enable_server_hostname_verification( server_hostname_verification_); redirect_client.system_ca_mode_ = system_ca_mode_; // Transfer CA certificate to redirect client if (!ca_cert_pem_.empty()) { redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(), ca_cert_pem_.size()); } if (!ca_cert_file_path_.empty()) { redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_); } // Client certificates are set through constructor for SSLClient // NOTE: SSLClient constructor already takes client_cert_path and // client_key_path so we need to create it properly if client certs are // needed // Execute the redirect return detail::redirect(redirect_client, req, res, path, location, error); #else // SSL not supported - set appropriate error error = Error::SSLConnection; output_error_log(error, &req); return false; #endif } else { // HTTP redirect ClientImpl redirect_client(host, port); // Setup client with robust configuration setup_redirect_client(redirect_client); // Execute the redirect return detail::redirect(redirect_client, req, res, path, location, error); } } // New method for robust client setup (based on basic_manual_redirect.cpp // logic) template void ClientImpl::setup_redirect_client(ClientType &client) { // Copy basic settings first client.set_connection_timeout(connection_timeout_sec_); client.set_read_timeout(read_timeout_sec_, read_timeout_usec_); client.set_write_timeout(write_timeout_sec_, write_timeout_usec_); client.set_keep_alive(keep_alive_); client.set_follow_location( true); // Enable redirects to handle multi-step redirects client.set_path_encode(path_encode_); client.set_compress(compress_); client.set_decompress(decompress_); // NOTE: Authentication credentials (basic auth, bearer token, digest auth) // are intentionally NOT copied to the redirect client. Per RFC 9110 Section // 15.4, credentials must not be forwarded when redirecting to a different // host. This function is only called for cross-host redirects; same-host // redirects are handled directly in ClientImpl::redirect(). // Copy the proxy configuration unconditionally; the per-target bypass is // re-evaluated at send time, so a later hop to a non-bypassed host can // still use the proxy. client.no_proxy_entries_ = no_proxy_entries_; if (!proxy_host_.empty() && proxy_port_ != -1) { client.set_proxy(proxy_host_, proxy_port_); if (!proxy_basic_auth_username_.empty()) { client.set_proxy_basic_auth(proxy_basic_auth_username_, proxy_basic_auth_password_); } if (!proxy_bearer_token_auth_token_.empty()) { client.set_proxy_bearer_token_auth(proxy_bearer_token_auth_token_); } #ifdef CPPHTTPLIB_SSL_ENABLED if (!proxy_digest_auth_username_.empty()) { client.set_proxy_digest_auth(proxy_digest_auth_username_, proxy_digest_auth_password_); } #endif } // Copy network and socket settings client.set_address_family(address_family_); client.set_tcp_nodelay(tcp_nodelay_); client.set_ipv6_v6only(ipv6_v6only_); if (socket_options_) { client.set_socket_options(socket_options_); } if (!interface_.empty()) { client.set_interface(interface_); } // Copy logging and headers if (logger_) { client.set_logger(logger_); } if (error_logger_) { client.set_error_logger(error_logger_); } // NOTE: DO NOT copy default_headers_ as they may contain stale Host headers // Each new client should generate its own headers based on its target host } bool ClientImpl::write_content_with_provider(Stream &strm, const Request &req, Error &error) const { auto is_shutting_down = []() { return false; }; if (req.is_chunked_content_provider_) { auto compressor = compress_ ? detail::create_compressor().first : std::unique_ptr(); if (!compressor) { compressor = detail::make_unique(); } return detail::write_content_chunked(strm, req.content_provider_, is_shutting_down, *compressor, error); } else { return detail::write_content_with_progress( strm, req.content_provider_, 0, req.content_length_, is_shutting_down, req.upload_progress, error); } } bool ClientImpl::write_request(Stream &strm, Request &req, bool close_connection, Error &error, bool skip_body) { // Prepare additional headers if (close_connection) { if (!req.has_header("Connection")) { req.set_header("Connection", "close"); } } std::string ct_for_defaults; if (!req.has_header("Content-Type") && !req.body.empty()) { ct_for_defaults = "text/plain"; } prepare_default_headers(req, false, ct_for_defaults); if (req.body.empty()) { if (req.content_provider_) { if (!req.is_chunked_content_provider_) { if (!req.has_header("Content-Length")) { auto length = std::to_string(req.content_length_); req.set_header("Content-Length", length); } } } else { if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH") { req.set_header("Content-Length", "0"); } } } if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) { if (!req.has_header("Authorization")) { req.headers.insert(make_basic_authentication_header( basic_auth_username_, basic_auth_password_, false)); } } if (!bearer_token_auth_token_.empty()) { if (!req.has_header("Authorization")) { req.headers.insert(make_bearer_token_authentication_header( bearer_token_auth_token_, false)); } } // Proxy-Authorization is only sent when the proxy is actually used for // this target โ€” otherwise NO_PROXY-matched requests would leak proxy // credentials directly to the destination server. if (is_proxy_enabled_for_host(host_)) { if (!proxy_basic_auth_username_.empty() && !proxy_basic_auth_password_.empty() && !req.has_header("Proxy-Authorization")) { req.headers.insert(make_basic_authentication_header( proxy_basic_auth_username_, proxy_basic_auth_password_, true)); } if (!proxy_bearer_token_auth_token_.empty() && !req.has_header("Proxy-Authorization")) { req.headers.insert(make_bearer_token_authentication_header( proxy_bearer_token_auth_token_, true)); } } // Request line and headers { detail::BufferStream bstrm; // Extract path and query from req.path std::string path_part, query_part; auto query_pos = req.path.find('?'); if (query_pos != std::string::npos) { path_part = req.path.substr(0, query_pos); query_part = req.path.substr(query_pos + 1); } else { path_part = req.path; query_part = ""; } // Encode path part. If the original `req.path` already contained a // query component, preserve its raw query string (including parameter // order) instead of reparsing and reassembling it which may reorder // parameters due to container ordering (e.g. `Params` uses // `std::multimap`). When there is no query in `req.path`, fall back to // building a query from `req.params` so existing callers that pass // `Params` continue to work. auto path_with_query = path_encode_ ? detail::encode_path(path_part) : path_part; if (!query_part.empty()) { // Normalize the query string (decode then re-encode) while preserving // the original parameter order. auto normalized = detail::normalize_query_string(query_part); if (!normalized.empty()) { path_with_query += '?' + normalized; } // Still populate req.params for handlers/users who read them. detail::parse_query_text(query_part, req.params); } else { // No query in path; parse any query_part (empty) and append params // from `req.params` when present (preserves prior behavior for // callers who provide Params separately). detail::parse_query_text(query_part, req.params); if (!req.params.empty()) { path_with_query = append_query_params(path_with_query, req.params); } } // Write request line and headers detail::write_request_line(bstrm, req.method, path_with_query); if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_, error)) { output_error_log(error, &req); return false; } // Flush buffer auto &data = bstrm.get_buffer(); if (!detail::write_data(strm, data.data(), data.size())) { error = Error::Write; output_error_log(error, &req); return false; } } // After sending request line and headers, wait briefly for an early server // response (e.g. 4xx) and avoid sending a potentially large request body // unnecessarily. This workaround is only enabled on Windows because Unix // platforms surface write errors (EPIPE) earlier; on Windows kernel send // buffering can accept large writes even when the peer already responded. // Check the stream first (which covers SSL via `is_readable()`), then // fall back to select on the socket. Only perform the wait for very large // request bodies to avoid interfering with normal small requests and // reduce side-effects. Poll briefly (up to 50ms as default) for an early // response. Skip this check when using Expect: 100-continue, as the protocol // handles early responses properly. #if defined(_WIN32) if (!skip_body && req.body.size() > CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD && req.path.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { auto start = std::chrono::high_resolution_clock::now(); for (;;) { // Prefer socket-level readiness to avoid SSL_pending() false-positives // from SSL internals. If the underlying socket is readable, assume an // early response may be present. auto sock = strm.socket(); if (sock != INVALID_SOCKET && detail::select_read(sock, 0, 0) > 0) { return false; } // Fallback to stream-level check for non-socket streams or when the // socket isn't reporting readable. Avoid using `is_readable()` for // SSL, since `SSL_pending()` may report buffered records that do not // indicate a complete application-level response yet. if (!is_ssl() && strm.is_readable()) { return false; } auto now = std::chrono::high_resolution_clock::now(); auto elapsed = std::chrono::duration_cast(now - start) .count(); if (elapsed >= CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND) { break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } #endif // Body if (skip_body) { return true; } return write_request_body(strm, req, error); } bool ClientImpl::write_request_body(Stream &strm, Request &req, Error &error) { if (req.body.empty()) { return write_content_with_provider(strm, req, error); } if (req.upload_progress) { auto body_size = req.body.size(); size_t written = 0; auto data = req.body.data(); while (written < body_size) { size_t to_write = (std::min)(CPPHTTPLIB_SEND_BUFSIZ, body_size - written); if (!detail::write_data(strm, data + written, to_write)) { error = Error::Write; output_error_log(error, &req); return false; } written += to_write; if (!req.upload_progress(written, body_size)) { error = Error::Canceled; output_error_log(error, &req); return false; } } } else { if (!detail::write_data(strm, req.body.data(), req.body.size())) { error = Error::Write; output_error_log(error, &req); return false; } } return true; } std::unique_ptr ClientImpl::send_with_content_provider_and_receiver( Request &req, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type, ContentReceiver content_receiver, Error &error) { if (!content_type.empty()) { req.set_header("Content-Type", content_type); } auto enc = compress_ ? detail::create_compressor() : std::pair, const char *>( nullptr, nullptr); if (enc.second) { req.set_header("Content-Encoding", enc.second); } if (enc.first && !content_provider_without_length) { auto &compressor = enc.first; if (content_provider) { auto ok = true; size_t offset = 0; DataSink data_sink; data_sink.write = [&](const char *data, size_t data_len) -> bool { if (ok) { auto last = offset + data_len == content_length; auto ret = compressor->compress( data, data_len, last, [&](const char *compressed_data, size_t compressed_data_len) { req.body.append(compressed_data, compressed_data_len); return true; }); if (ret) { offset += data_len; } else { ok = false; } } return ok; }; while (ok && offset < content_length) { if (!content_provider(offset, content_length - offset, data_sink)) { error = Error::Canceled; output_error_log(error, &req); return nullptr; } } } else { if (!compressor->compress(body, content_length, true, [&](const char *data, size_t data_len) { req.body.append(data, data_len); return true; })) { error = Error::Compression; output_error_log(error, &req); return nullptr; } } } else { if (content_provider) { req.content_length_ = content_length; req.content_provider_ = std::move(content_provider); req.is_chunked_content_provider_ = false; } else if (content_provider_without_length) { req.content_length_ = 0; req.content_provider_ = detail::ContentProviderAdapter( std::move(content_provider_without_length)); req.is_chunked_content_provider_ = true; req.set_header("Transfer-Encoding", "chunked"); } else { req.body.assign(body, content_length); } } if (content_receiver) { req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; } auto res = detail::make_unique(); return send(req, *res, error) ? std::move(res) : nullptr; } Result ClientImpl::send_with_content_provider_and_receiver( const std::string &method, const std::string &path, const Headers &headers, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { Request req; req.method = method; req.headers = headers; req.path = path; req.upload_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } auto error = Error::Success; auto res = send_with_content_provider_and_receiver( req, body, content_length, std::move(content_provider), std::move(content_provider_without_length), content_type, std::move(content_receiver), error); #ifdef CPPHTTPLIB_SSL_ENABLED return Result{std::move(res), error, std::move(req.headers), last_ssl_error_, last_backend_error_}; #else return Result{std::move(res), error, std::move(req.headers)}; #endif } void ClientImpl::output_log(const Request &req, const Response &res) const { if (logger_) { std::lock_guard guard(logger_mutex_); logger_(req, res); } } void ClientImpl::output_error_log(const Error &err, const Request *req) const { if (error_logger_) { std::lock_guard guard(logger_mutex_); error_logger_(err, req); } } bool ClientImpl::process_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error) { // Auto-add Expect: 100-continue for large bodies if (CPPHTTPLIB_EXPECT_100_THRESHOLD > 0 && !req.has_header("Expect")) { auto body_size = req.body.empty() ? req.content_length_ : req.body.size(); if (body_size >= CPPHTTPLIB_EXPECT_100_THRESHOLD) { req.set_header("Expect", "100-continue"); } } // Check for Expect: 100-continue auto expect_100_continue = req.get_header_value("Expect") == "100-continue"; // Send request (skip body if using Expect: 100-continue) auto write_request_success = write_request(strm, req, close_connection, error, expect_100_continue); #ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl() && !expect_100_continue) { auto is_proxy_enabled = is_proxy_enabled_for_host(host_); if (!is_proxy_enabled) { if (tls::is_peer_closed(socket_.ssl, socket_.sock)) { error = Error::SSLPeerCouldBeClosed_; output_error_log(error, &req); return false; } } } #endif // Handle Expect: 100-continue. // // Wait for an interim/early response by attempting to read the status line // under a short timeout, instead of trusting raw socket readability. Over // TLS, post-handshake records (e.g. session tickets) make the socket // readable without any HTTP response being available; relying on // `select_read` there caused the body to be withheld forever and the // request to fail with `Read` (#2458). If no status line arrives within the // timeout, send the body anyway (matching curl's behavior). auto status_line_read = false; if (expect_100_continue && write_request_success) { if (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND > 0) { time_t sec = CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND / 1000; time_t usec = (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND % 1000) * 1000; strm.set_read_timeout(sec, usec); status_line_read = read_response_line(strm, req, res, false); strm.set_read_timeout(read_timeout_sec_, read_timeout_usec_); } if (!status_line_read) { // No interim response within the timeout: send the body and handle the // response as usual. if (!write_request_body(strm, req, error)) { return false; } expect_100_continue = false; // Switch to normal response handling } } // Receive response and headers // When using Expect: 100-continue, don't auto-skip `100 Continue` response if ((!status_line_read && !read_response_line(strm, req, res, !expect_100_continue)) || !detail::read_headers(strm, res.headers)) { if (write_request_success) { error = Error::Read; } output_error_log(error, &req); return false; } if (!write_request_success) { return false; } // Handle Expect: 100-continue response if (expect_100_continue) { if (res.status == StatusCode::Continue_100) { // Server accepted, send the body if (!write_request_body(strm, req, error)) { return false; } // Read the actual response res.headers.clear(); res.body.clear(); if (!read_response_line(strm, req, res) || !detail::read_headers(strm, res.headers)) { error = Error::Read; output_error_log(error, &req); return false; } } // If not 100 Continue, server returned an error; proceed with that response } // Body if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" && req.method != "CONNECT") { auto redirect = 300 < res.status && res.status < 400 && res.status != StatusCode::NotModified_304 && follow_location_; if (req.response_handler && !redirect) { if (!req.response_handler(res)) { error = Error::Canceled; output_error_log(error, &req); return false; } } auto out = req.content_receiver ? static_cast( [&](const char *buf, size_t n, size_t off, size_t len) { if (redirect) { return true; } auto ret = req.content_receiver(buf, n, off, len); if (!ret) { error = Error::Canceled; output_error_log(error, &req); } return ret; }) : static_cast( [&](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) { assert(res.body.size() + n <= res.body.max_size()); if (payload_max_length_ > 0 && (res.body.size() >= payload_max_length_ || n > payload_max_length_ - res.body.size())) { return false; } res.body.append(buf, n); return true; }); auto progress = [&](size_t current, size_t total) { if (!req.download_progress || redirect) { return true; } auto ret = req.download_progress(current, total); if (!ret) { error = Error::Canceled; output_error_log(error, &req); } return ret; }; if (res.has_header("Content-Length")) { if (!req.content_receiver) { auto len = res.get_header_value_u64("Content-Length"); if (len > res.body.max_size()) { error = Error::Read; output_error_log(error, &req); return false; } // Cap the reservation by payload_max_length_ to avoid OOM when a // hostile or malformed server sends an enormous Content-Length. // The actual body read below is bounded by payload_max_length_, // so reserving more than that is never useful. auto reserve_len = static_cast(len); if (payload_max_length_ > 0 && reserve_len > payload_max_length_) { reserve_len = payload_max_length_; } res.body.reserve(reserve_len); } } if (res.status != StatusCode::NotModified_304) { int dummy_status; auto max_length = (!has_payload_max_length_ && req.content_receiver) ? (std::numeric_limits::max)() : payload_max_length_; if (!detail::read_content(strm, res, max_length, dummy_status, std::move(progress), std::move(out), decompress_)) { if (error != Error::Canceled) { error = Error::Read; } output_error_log(error, &req); return false; } } } // Log output_log(req, res); return true; } ContentProviderWithoutLength ClientImpl::get_multipart_content_provider( const std::string &boundary, const UploadFormDataItems &items, const FormDataProviderItems &provider_items) const { size_t cur_item = 0; size_t cur_start = 0; // cur_item and cur_start are copied to within the std::function and // maintain state between successive calls return [&, cur_item, cur_start](size_t offset, DataSink &sink) mutable -> bool { if (!offset && !items.empty()) { sink.os << detail::serialize_multipart_formdata(items, boundary, false); return true; } else if (cur_item < provider_items.size()) { if (!cur_start) { const auto &begin = detail::serialize_multipart_formdata_item_begin( provider_items[cur_item], boundary); offset += begin.size(); cur_start = offset; sink.os << begin; } DataSink cur_sink; auto has_data = true; cur_sink.write = sink.write; cur_sink.done = [&]() { has_data = false; }; if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) { return false; } if (!has_data) { sink.os << detail::serialize_multipart_formdata_item_end(); cur_item++; cur_start = 0; } return true; } else { sink.os << detail::serialize_multipart_formdata_finish(boundary); sink.done(); return true; } }; } bool ClientImpl::process_socket( const Socket &socket, std::chrono::time_point start_time, std::function callback) { return detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, std::move(callback)); } bool ClientImpl::is_ssl() const { return false; } Result ClientImpl::Get(const std::string &path, DownloadProgress progress) { return Get(path, Headers(), std::move(progress)); } Result ClientImpl::Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress) { if (params.empty()) { return Get(path, headers); } std::string path_with_query = append_query_params(path, params); return Get(path_with_query, headers, std::move(progress)); } Result ClientImpl::Get(const std::string &path, const Headers &headers, DownloadProgress progress) { Request req; req.method = "GET"; req.path = path; req.headers = headers; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } return send_(std::move(req)); } Result ClientImpl::Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress) { return Get(path, Headers(), nullptr, std::move(content_receiver), std::move(progress)); } Result ClientImpl::Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress) { return Get(path, headers, nullptr, std::move(content_receiver), std::move(progress)); } Result ClientImpl::Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { return Get(path, Headers(), std::move(response_handler), std::move(content_receiver), std::move(progress)); } Result ClientImpl::Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { Request req; req.method = "GET"; req.path = path; req.headers = headers; req.response_handler = std::move(response_handler); req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } return send_(std::move(req)); } Result ClientImpl::Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress) { return Get(path, params, headers, nullptr, std::move(content_receiver), std::move(progress)); } Result ClientImpl::Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { if (params.empty()) { return Get(path, headers, std::move(response_handler), std::move(content_receiver), std::move(progress)); } std::string path_with_query = append_query_params(path, params); return Get(path_with_query, headers, std::move(response_handler), std::move(content_receiver), std::move(progress)); } Result ClientImpl::Head(const std::string &path) { return Head(path, Headers()); } Result ClientImpl::Head(const std::string &path, const Headers &headers) { Request req; req.method = "HEAD"; req.headers = headers; req.path = path; if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } return send_(std::move(req)); } Result ClientImpl::Post(const std::string &path) { return Post(path, std::string(), std::string()); } Result ClientImpl::Post(const std::string &path, const Headers &headers) { return Post(path, headers, nullptr, 0, std::string()); } Result ClientImpl::Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return Post(path, Headers(), body, content_length, content_type, progress); } Result ClientImpl::Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return Post(path, Headers(), body, content_type, progress); } Result ClientImpl::Post(const std::string &path, const Params ¶ms) { return Post(path, Headers(), params); } Result ClientImpl::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return Post(path, Headers(), content_length, std::move(content_provider), content_type, progress); } Result ClientImpl::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Post(path, Headers(), content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result ClientImpl::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return Post(path, Headers(), std::move(content_provider), content_type, progress); } Result ClientImpl::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Post(path, Headers(), std::move(content_provider), content_type, std::move(content_receiver), progress); } Result ClientImpl::Post(const std::string &path, const Headers &headers, const Params ¶ms) { auto query = detail::params_to_query_str(params); return Post(path, headers, query, "application/x-www-form-urlencoded"); } Result ClientImpl::Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return Post(path, Headers(), items, progress); } Result ClientImpl::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); auto content_length = detail::get_multipart_content_length(items, boundary); return Post(path, headers, content_length, detail::make_multipart_content_provider(items, boundary), content_type, progress); } Result ClientImpl::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { if (!detail::is_multipart_boundary_chars_valid(boundary)) { return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; } const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); auto content_length = detail::get_multipart_content_length(items, boundary); return Post(path, headers, content_length, detail::make_multipart_content_provider(items, boundary), content_type, progress); } Result ClientImpl::Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, body, content_length, nullptr, nullptr, content_type, nullptr, progress); } Result ClientImpl::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, body.data(), body.size(), nullptr, nullptr, content_type, nullptr, progress); } Result ClientImpl::Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, nullptr, progress); } Result ClientImpl::Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, std::move(content_receiver), std::move(progress)); } Result ClientImpl::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, nullptr, progress); } Result ClientImpl::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, std::move(content_receiver), std::move(progress)); } Result ClientImpl::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); return send_with_content_provider_and_receiver( "POST", path, headers, nullptr, 0, nullptr, get_multipart_content_provider(boundary, items, provider_items), content_type, nullptr, progress); } Result ClientImpl::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { Request req; req.method = "POST"; req.path = path; req.headers = headers; req.body = body; req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } if (!content_type.empty()) { req.set_header("Content-Type", content_type); } return send_(std::move(req)); } Result ClientImpl::Put(const std::string &path) { return Put(path, std::string(), std::string()); } Result ClientImpl::Put(const std::string &path, const Headers &headers) { return Put(path, headers, nullptr, 0, std::string()); } Result ClientImpl::Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return Put(path, Headers(), body, content_length, content_type, progress); } Result ClientImpl::Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return Put(path, Headers(), body, content_type, progress); } Result ClientImpl::Put(const std::string &path, const Params ¶ms) { return Put(path, Headers(), params); } Result ClientImpl::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return Put(path, Headers(), content_length, std::move(content_provider), content_type, progress); } Result ClientImpl::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Put(path, Headers(), content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result ClientImpl::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return Put(path, Headers(), std::move(content_provider), content_type, progress); } Result ClientImpl::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Put(path, Headers(), std::move(content_provider), content_type, std::move(content_receiver), progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, const Params ¶ms) { auto query = detail::params_to_query_str(params); return Put(path, headers, query, "application/x-www-form-urlencoded"); } Result ClientImpl::Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return Put(path, Headers(), items, progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); auto content_length = detail::get_multipart_content_length(items, boundary); return Put(path, headers, content_length, detail::make_multipart_content_provider(items, boundary), content_type, progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { if (!detail::is_multipart_boundary_chars_valid(boundary)) { return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; } const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); auto content_length = detail::get_multipart_content_length(items, boundary); return Put(path, headers, content_length, detail::make_multipart_content_provider(items, boundary), content_type, progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, body, content_length, nullptr, nullptr, content_type, nullptr, progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, body.data(), body.size(), nullptr, nullptr, content_type, nullptr, progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, nullptr, progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, std::move(content_receiver), progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, nullptr, progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); return send_with_content_provider_and_receiver( "PUT", path, headers, nullptr, 0, nullptr, get_multipart_content_provider(boundary, items, provider_items), content_type, nullptr, progress); } Result ClientImpl::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { Request req; req.method = "PUT"; req.path = path; req.headers = headers; req.body = body; req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } if (!content_type.empty()) { req.set_header("Content-Type", content_type); } return send_(std::move(req)); } Result ClientImpl::Patch(const std::string &path) { return Patch(path, std::string(), std::string()); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, UploadProgress progress) { return Patch(path, headers, nullptr, 0, std::string(), progress); } Result ClientImpl::Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return Patch(path, Headers(), body, content_length, content_type, progress); } Result ClientImpl::Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return Patch(path, Headers(), body, content_type, progress); } Result ClientImpl::Patch(const std::string &path, const Params ¶ms) { return Patch(path, Headers(), params); } Result ClientImpl::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return Patch(path, Headers(), content_length, std::move(content_provider), content_type, progress); } Result ClientImpl::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Patch(path, Headers(), content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result ClientImpl::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return Patch(path, Headers(), std::move(content_provider), content_type, progress); } Result ClientImpl::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return Patch(path, Headers(), std::move(content_provider), content_type, std::move(content_receiver), progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, const Params ¶ms) { auto query = detail::params_to_query_str(params); return Patch(path, headers, query, "application/x-www-form-urlencoded"); } Result ClientImpl::Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return Patch(path, Headers(), items, progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); auto content_length = detail::get_multipart_content_length(items, boundary); return Patch(path, headers, content_length, detail::make_multipart_content_provider(items, boundary), content_type, progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { if (!detail::is_multipart_boundary_chars_valid(boundary)) { return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; } const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); auto content_length = detail::get_multipart_content_length(items, boundary); return Patch(path, headers, content_length, detail::make_multipart_content_provider(items, boundary), content_type, progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, body, content_length, nullptr, nullptr, content_type, nullptr, progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, body.data(), body.size(), nullptr, nullptr, content_type, nullptr, progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, nullptr, progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type, std::move(content_receiver), progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, nullptr, progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); return send_with_content_provider_and_receiver( "PATCH", path, headers, nullptr, 0, nullptr, get_multipart_content_provider(boundary, items, provider_items), content_type, nullptr, progress); } Result ClientImpl::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { Request req; req.method = "PATCH"; req.path = path; req.headers = headers; req.body = body; req.content_receiver = [content_receiver](const char *data, size_t data_length, size_t /*offset*/, size_t /*total_length*/) { return content_receiver(data, data_length); }; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } if (!content_type.empty()) { req.set_header("Content-Type", content_type); } return send_(std::move(req)); } Result ClientImpl::Delete(const std::string &path, DownloadProgress progress) { return Delete(path, Headers(), std::string(), std::string(), progress); } Result ClientImpl::Delete(const std::string &path, const Headers &headers, DownloadProgress progress) { return Delete(path, headers, std::string(), std::string(), progress); } Result ClientImpl::Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress) { return Delete(path, Headers(), body, content_length, content_type, progress); } Result ClientImpl::Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress) { return Delete(path, Headers(), body.data(), body.size(), content_type, progress); } Result ClientImpl::Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress) { return Delete(path, headers, body.data(), body.size(), content_type, progress); } Result ClientImpl::Delete(const std::string &path, const Params ¶ms, DownloadProgress progress) { return Delete(path, Headers(), params, progress); } Result ClientImpl::Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress) { auto query = detail::params_to_query_str(params); return Delete(path, headers, query, "application/x-www-form-urlencoded", progress); } Result ClientImpl::Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress) { Request req; req.method = "DELETE"; req.headers = headers; req.path = path; req.download_progress = std::move(progress); if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } if (!content_type.empty()) { req.set_header("Content-Type", content_type); } req.body.assign(body, content_length); return send_(std::move(req)); } Result ClientImpl::Options(const std::string &path) { return Options(path, Headers()); } Result ClientImpl::Options(const std::string &path, const Headers &headers) { Request req; req.method = "OPTIONS"; req.headers = headers; req.path = path; if (max_timeout_msec_ > 0) { req.start_time_ = std::chrono::steady_clock::now(); } return send_(std::move(req)); } void ClientImpl::stop() { std::lock_guard guard(socket_mutex_); // If there is anything ongoing right now, the ONLY thread-safe thing we can // do is to shutdown_socket, so that threads using this socket suddenly // discover they can't read/write any more and error out. Everything else // (closing the socket, shutting ssl down) is unsafe because these actions // are not thread-safe. if (socket_requests_in_flight_ > 0) { shutdown_socket(socket_); // Aside from that, we set a flag for the socket to be closed when we're // done. socket_should_be_closed_when_request_is_done_ = true; return; } disconnect(/*gracefully=*/true); } std::string ClientImpl::host() const { return host_; } int ClientImpl::port() const { return port_; } size_t ClientImpl::is_socket_open() const { std::lock_guard guard(socket_mutex_); return socket_.is_open(); } socket_t ClientImpl::socket() const { return socket_.sock; } void ClientImpl::set_connection_timeout(time_t sec, time_t usec) { connection_timeout_sec_ = sec; connection_timeout_usec_ = usec; } void ClientImpl::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; } void ClientImpl::set_write_timeout(time_t sec, time_t usec) { write_timeout_sec_ = sec; write_timeout_usec_ = usec; } void ClientImpl::set_max_timeout(time_t msec) { max_timeout_msec_ = msec; } void ClientImpl::set_basic_auth(const std::string &username, const std::string &password) { basic_auth_username_ = username; basic_auth_password_ = password; } void ClientImpl::set_bearer_token_auth(const std::string &token) { bearer_token_auth_token_ = token; } void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; } void ClientImpl::set_follow_location(bool on) { follow_location_ = on; } void ClientImpl::set_path_encode(bool on) { path_encode_ = on; } void ClientImpl::set_hostname_addr_map(std::map addr_map) { addr_map_ = std::move(addr_map); } void ClientImpl::set_default_headers(Headers headers) { default_headers_ = std::move(headers); } void ClientImpl::set_header_writer( std::function const &writer) { header_writer_ = writer; } void ClientImpl::set_address_family(int family) { address_family_ = family; } void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; } void ClientImpl::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; } void ClientImpl::set_socket_options(SocketOptions socket_options) { socket_options_ = std::move(socket_options); } void ClientImpl::set_compress(bool on) { compress_ = on; } void ClientImpl::set_decompress(bool on) { decompress_ = on; } void ClientImpl::set_payload_max_length(size_t length) { payload_max_length_ = length; has_payload_max_length_ = true; } void ClientImpl::set_interface(const std::string &intf) { interface_ = intf; } void ClientImpl::set_proxy(const std::string &host, int port) { proxy_host_ = host; proxy_port_ = port; std::lock_guard guard(socket_mutex_); disconnect(/*gracefully=*/true); } void ClientImpl::set_proxy_basic_auth(const std::string &username, const std::string &password) { proxy_basic_auth_username_ = username; proxy_basic_auth_password_ = password; } void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) { proxy_bearer_token_auth_token_ = token; } void ClientImpl::set_no_proxy(const std::vector &patterns) { std::vector parsed; parsed.reserve(patterns.size()); for (const auto &p : patterns) { auto trimmed = detail::trim_copy(p); if (trimmed.empty()) { continue; } detail::NoProxyEntry entry; if (detail::parse_no_proxy_entry(trimmed, entry)) { parsed.push_back(std::move(entry)); } } no_proxy_entries_ = std::move(parsed); std::lock_guard guard(socket_mutex_); disconnect(/*gracefully=*/true); } #ifdef CPPHTTPLIB_SSL_ENABLED void ClientImpl::set_digest_auth(const std::string &username, const std::string &password) { digest_auth_username_ = username; digest_auth_password_ = password; } void ClientImpl::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 ClientImpl::set_proxy_digest_auth(const std::string &username, const std::string &password) { proxy_digest_auth_username_ = username; proxy_digest_auth_password_ = password; } void ClientImpl::enable_server_certificate_verification(bool enabled) { server_certificate_verification_ = enabled; } void ClientImpl::enable_server_hostname_verification(bool enabled) { server_hostname_verification_ = enabled; } void ClientImpl::enable_system_ca(bool enabled) { system_ca_mode_ = enabled ? SystemCAMode::Enabled : SystemCAMode::Disabled; } #endif void ClientImpl::set_logger(Logger logger) { logger_ = std::move(logger); } void ClientImpl::set_error_logger(ErrorLogger error_logger) { error_logger_ = std::move(error_logger); } /* * SSL/TLS Common Implementation */ ClientConnection::~ClientConnection() { #ifdef CPPHTTPLIB_SSL_ENABLED if (session) { tls::shutdown(session, true); tls::free_session(session); session = nullptr; } #endif if (sock != INVALID_SOCKET) { detail::close_socket(sock); sock = INVALID_SOCKET; } } // Universal client implementation Client::Client(const std::string &scheme_host_port) : Client(scheme_host_port, std::string(), std::string()) {} Client::Client(const std::string &scheme_host_port, const std::string &client_cert_path, const std::string &client_key_path) { detail::UrlComponents uc; if (detail::parse_url(scheme_host_port, uc) && !uc.host.empty()) { auto &scheme = uc.scheme; #ifdef CPPHTTPLIB_SSL_ENABLED if (!scheme.empty() && (scheme != "http" && scheme != "https")) { #else if (!scheme.empty() && scheme != "http") { #endif #ifndef CPPHTTPLIB_NO_EXCEPTIONS std::string msg = "'" + scheme + "' scheme is not supported."; throw std::invalid_argument(msg); #endif return; } auto is_ssl = scheme == "https"; auto host = std::move(uc.host); auto port = is_ssl ? 443 : 80; if (!uc.port.empty() && !detail::parse_port(uc.port, port)) { return; } if (is_ssl) { #ifdef CPPHTTPLIB_SSL_ENABLED cli_ = detail::make_unique(host, port, client_cert_path, client_key_path); is_ssl_ = is_ssl; #endif } else { cli_ = detail::make_unique(host, port, client_cert_path, client_key_path); } } else { // NOTE: Update TEST(UniversalClientImplTest, Ipv6LiteralAddress) // if port param below changes. cli_ = detail::make_unique(scheme_host_port, 80, client_cert_path, client_key_path); } } Client::Client(const std::string &host, int port) : Client(host, port, std::string(), std::string()) {} Client::Client(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path) : cli_(detail::make_unique(host, port, client_cert_path, client_key_path)) {} Client::~Client() = default; bool Client::is_valid() const { return cli_ != nullptr && cli_->is_valid(); } Result Client::Get(const std::string &path, DownloadProgress progress) { return cli_->Get(path, std::move(progress)); } Result Client::Get(const std::string &path, const Headers &headers, DownloadProgress progress) { return cli_->Get(path, headers, std::move(progress)); } Result Client::Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, std::move(content_receiver), std::move(progress)); } Result Client::Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, headers, std::move(content_receiver), std::move(progress)); } Result Client::Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, std::move(response_handler), std::move(content_receiver), std::move(progress)); } Result Client::Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, headers, std::move(response_handler), std::move(content_receiver), std::move(progress)); } Result Client::Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress) { return cli_->Get(path, params, headers, std::move(progress)); } Result Client::Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, params, headers, std::move(content_receiver), std::move(progress)); } Result Client::Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Get(path, params, headers, std::move(response_handler), std::move(content_receiver), std::move(progress)); } Result Client::Head(const std::string &path) { return cli_->Head(path); } Result Client::Head(const std::string &path, const Headers &headers) { return cli_->Head(path, headers); } Result Client::Post(const std::string &path) { return cli_->Post(path); } Result Client::Post(const std::string &path, const Headers &headers) { return cli_->Post(path, headers); } Result Client::Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, body, content_length, content_type, progress); } Result Client::Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, headers, body, content_length, content_type, progress); } Result Client::Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, body, content_type, progress); } Result Client::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, headers, body, content_type, progress); } Result Client::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, content_length, std::move(content_provider), content_type, progress); } Result Client::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Post(path, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, std::move(content_provider), content_type, progress); } Result Client::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Post(path, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, headers, content_length, std::move(content_provider), content_type, progress); } Result Client::Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Post(path, headers, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Post(path, headers, std::move(content_provider), content_type, progress); } Result Client::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Post(path, headers, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Post(const std::string &path, const Params ¶ms) { return cli_->Post(path, params); } Result Client::Post(const std::string &path, const Headers &headers, const Params ¶ms) { return cli_->Post(path, headers, params); } Result Client::Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Post(path, items, progress); } Result Client::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Post(path, headers, items, progress); } Result Client::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { return cli_->Post(path, headers, items, boundary, progress); } Result Client::Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { return cli_->Post(path, headers, items, provider_items, progress); } Result Client::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Post(path, headers, body, content_type, std::move(content_receiver), progress); } Result Client::Put(const std::string &path) { return cli_->Put(path); } Result Client::Put(const std::string &path, const Headers &headers) { return cli_->Put(path, headers); } Result Client::Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, body, content_length, content_type, progress); } Result Client::Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, headers, body, content_length, content_type, progress); } Result Client::Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, body, content_type, progress); } Result Client::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, headers, body, content_type, progress); } Result Client::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, content_length, std::move(content_provider), content_type, progress); } Result Client::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Put(path, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, std::move(content_provider), content_type, progress); } Result Client::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Put(path, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, headers, content_length, std::move(content_provider), content_type, progress); } Result Client::Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Put(path, headers, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Put(path, headers, std::move(content_provider), content_type, progress); } Result Client::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Put(path, headers, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Put(const std::string &path, const Params ¶ms) { return cli_->Put(path, params); } Result Client::Put(const std::string &path, const Headers &headers, const Params ¶ms) { return cli_->Put(path, headers, params); } Result Client::Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Put(path, items, progress); } Result Client::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Put(path, headers, items, progress); } Result Client::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { return cli_->Put(path, headers, items, boundary, progress); } Result Client::Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { return cli_->Put(path, headers, items, provider_items, progress); } Result Client::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Put(path, headers, body, content_type, content_receiver, progress); } Result Client::Patch(const std::string &path) { return cli_->Patch(path); } Result Client::Patch(const std::string &path, const Headers &headers) { return cli_->Patch(path, headers); } Result Client::Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, body, content_length, content_type, progress); } Result Client::Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, headers, body, content_length, content_type, progress); } Result Client::Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, body, content_type, progress); } Result Client::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, headers, body, content_type, progress); } Result Client::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, content_length, std::move(content_provider), content_type, progress); } Result Client::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Patch(path, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, std::move(content_provider), content_type, progress); } Result Client::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Patch(path, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, headers, content_length, std::move(content_provider), content_type, progress); } Result Client::Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Patch(path, headers, content_length, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress) { return cli_->Patch(path, headers, std::move(content_provider), content_type, progress); } Result Client::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress) { return cli_->Patch(path, headers, std::move(content_provider), content_type, std::move(content_receiver), progress); } Result Client::Patch(const std::string &path, const Params ¶ms) { return cli_->Patch(path, params); } Result Client::Patch(const std::string &path, const Headers &headers, const Params ¶ms) { return cli_->Patch(path, headers, params); } Result Client::Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Patch(path, items, progress); } Result Client::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress) { return cli_->Patch(path, headers, items, progress); } Result Client::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress) { return cli_->Patch(path, headers, items, boundary, progress); } Result Client::Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress) { return cli_->Patch(path, headers, items, provider_items, progress); } Result Client::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress) { return cli_->Patch(path, headers, body, content_type, content_receiver, progress); } Result Client::Delete(const std::string &path, DownloadProgress progress) { return cli_->Delete(path, progress); } Result Client::Delete(const std::string &path, const Headers &headers, DownloadProgress progress) { return cli_->Delete(path, headers, progress); } Result Client::Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress) { return cli_->Delete(path, body, content_length, content_type, progress); } Result Client::Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress) { return cli_->Delete(path, headers, body, content_length, content_type, progress); } Result Client::Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress) { return cli_->Delete(path, body, content_type, progress); } Result Client::Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress) { return cli_->Delete(path, headers, body, content_type, progress); } Result Client::Delete(const std::string &path, const Params ¶ms, DownloadProgress progress) { return cli_->Delete(path, params, progress); } Result Client::Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress) { return cli_->Delete(path, headers, params, progress); } Result Client::Options(const std::string &path) { return cli_->Options(path); } Result Client::Options(const std::string &path, const Headers &headers) { return cli_->Options(path, headers); } ClientImpl::StreamHandle Client::open_stream(const std::string &method, const std::string &path, const Params ¶ms, const Headers &headers, const std::string &body, const std::string &content_type) { return cli_->open_stream(method, path, params, headers, body, content_type); } bool Client::send(Request &req, Response &res, Error &error) { return cli_->send(req, res, error); } Result Client::send(const Request &req) { return cli_->send(req); } void Client::stop() { cli_->stop(); } std::string Client::host() const { return cli_->host(); } int Client::port() const { return cli_->port(); } size_t Client::is_socket_open() const { return cli_->is_socket_open(); } socket_t Client::socket() const { return cli_->socket(); } void Client::set_hostname_addr_map(std::map addr_map) { cli_->set_hostname_addr_map(std::move(addr_map)); } void Client::set_default_headers(Headers headers) { cli_->set_default_headers(std::move(headers)); } void Client::set_header_writer( std::function const &writer) { cli_->set_header_writer(writer); } void Client::set_address_family(int family) { cli_->set_address_family(family); } void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); } void Client::set_socket_options(SocketOptions socket_options) { cli_->set_socket_options(std::move(socket_options)); } void Client::set_connection_timeout(time_t sec, time_t usec) { cli_->set_connection_timeout(sec, usec); } void Client::set_read_timeout(time_t sec, time_t usec) { cli_->set_read_timeout(sec, usec); } void Client::set_write_timeout(time_t sec, time_t usec) { cli_->set_write_timeout(sec, usec); } void Client::set_basic_auth(const std::string &username, const std::string &password) { cli_->set_basic_auth(username, password); } void Client::set_bearer_token_auth(const std::string &token) { cli_->set_bearer_token_auth(token); } void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); } void Client::set_follow_location(bool on) { cli_->set_follow_location(on); } void Client::set_path_encode(bool on) { cli_->set_path_encode(on); } void Client::set_compress(bool on) { cli_->set_compress(on); } void Client::set_decompress(bool on) { cli_->set_decompress(on); } void Client::set_payload_max_length(size_t length) { cli_->set_payload_max_length(length); } void Client::set_interface(const std::string &intf) { cli_->set_interface(intf); } void Client::set_proxy(const std::string &host, int port) { cli_->set_proxy(host, port); } void Client::set_proxy_basic_auth(const std::string &username, const std::string &password) { cli_->set_proxy_basic_auth(username, password); } void Client::set_proxy_bearer_token_auth(const std::string &token) { cli_->set_proxy_bearer_token_auth(token); } void Client::set_no_proxy(const std::vector &patterns) { cli_->set_no_proxy(patterns); } void Client::set_logger(Logger logger) { cli_->set_logger(std::move(logger)); } void Client::set_error_logger(ErrorLogger error_logger) { cli_->set_error_logger(std::move(error_logger)); } /* * Group 6: SSL Server and Client implementation */ #ifdef CPPHTTPLIB_SSL_ENABLED // SSL HTTP server implementation SSLServer::SSLServer(const char *cert_path, const char *private_key_path, const char *client_ca_cert_file_path, const char *client_ca_cert_dir_path, const char *private_key_password) { using namespace tls; ctx_ = create_server_context(); if (!ctx_) { return; } // Load server certificate and private key if (!set_server_cert_file(ctx_, cert_path, private_key_path, private_key_password)) { last_ssl_error_ = static_cast(get_error()); free_context(ctx_); ctx_ = nullptr; return; } // Load client CA certificates for client authentication if (client_ca_cert_file_path || client_ca_cert_dir_path) { if (!set_client_ca_file(ctx_, client_ca_cert_file_path, client_ca_cert_dir_path)) { last_ssl_error_ = static_cast(get_error()); free_context(ctx_); ctx_ = nullptr; return; } // Enable client certificate verification set_verify_client(ctx_, true); } } SSLServer::SSLServer(const PemMemory &pem) { using namespace tls; ctx_ = create_server_context(); if (ctx_) { if (!set_server_cert_pem(ctx_, pem.cert_pem, pem.key_pem, pem.private_key_password)) { last_ssl_error_ = static_cast(get_error()); free_context(ctx_); ctx_ = nullptr; } else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) { if (!load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) { last_ssl_error_ = static_cast(get_error()); free_context(ctx_); ctx_ = nullptr; } else { set_verify_client(ctx_, true); } } } } SSLServer::SSLServer(const tls::ContextSetupCallback &setup_callback) { using namespace tls; ctx_ = create_server_context(); if (ctx_) { if (!setup_callback(ctx_)) { free_context(ctx_); ctx_ = nullptr; } } } SSLServer::~SSLServer() { if (ctx_) { tls::free_context(ctx_); } } bool SSLServer::is_valid() const { return ctx_ != nullptr; } bool SSLServer::process_and_close_socket(socket_t sock) { using namespace tls; // Create TLS session with mutex protection session_t session = nullptr; { std::lock_guard guard(ctx_mutex_); session = create_session(static_cast(ctx_), sock); } if (!session) { last_ssl_error_ = static_cast(get_error()); detail::shutdown_socket(sock); detail::close_socket(sock); return false; } // Use scope_exit to ensure cleanup on all paths (including exceptions) bool handshake_done = false; bool ret = false; bool websocket_upgraded = false; auto cleanup = detail::scope_exit([&] { if (handshake_done) { shutdown(session, !websocket_upgraded && ret); } free_session(session); detail::shutdown_socket(sock); detail::close_socket(sock); }); // Perform TLS accept handshake with timeout TlsError tls_err; if (!accept_nonblocking(session, sock, read_timeout_sec_, read_timeout_usec_, &tls_err)) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // Map TlsError to legacy ssl_error for backward compatibility if (tls_err.code == ErrorCode::WantRead) { last_ssl_error_ = SSL_ERROR_WANT_READ; } else if (tls_err.code == ErrorCode::WantWrite) { last_ssl_error_ = SSL_ERROR_WANT_WRITE; } else { last_ssl_error_ = SSL_ERROR_SSL; } #else last_ssl_error_ = static_cast(get_error()); #endif return false; } handshake_done = true; std::string remote_addr; int remote_port = 0; detail::get_remote_ip_and_port(sock, remote_addr, remote_port); std::string local_addr; int local_port = 0; detail::get_local_ip_and_port(sock, local_addr, local_port); ret = detail::process_server_socket_ssl( svr_sock_, session, sock, keep_alive_max_count_, keep_alive_timeout_sec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, [&](Stream &strm, bool close_connection, bool &connection_closed) { return process_request( strm, remote_addr, remote_port, local_addr, local_port, close_connection, connection_closed, [&](Request &req) { req.ssl = session; }, &websocket_upgraded); }); return ret; } bool SSLServer::update_certs_pem(const char *cert_pem, const char *key_pem, const char *client_ca_pem, const char *password) { if (!ctx_) { return false; } std::lock_guard guard(ctx_mutex_); if (!tls::update_server_cert(ctx_, cert_pem, key_pem, password)) { return false; } if (client_ca_pem) { return tls::update_server_client_ca(ctx_, client_ca_pem); } return true; } // SSL HTTP client implementation SSLClient::~SSLClient() { if (ctx_) { tls::free_context(ctx_); } // Make sure to shut down SSL since shutdown_ssl will resolve to the // base function rather than the derived function once we get to the // base class destructor, and won't free the SSL (causing a leak). shutdown_ssl_impl(socket_, true); } bool SSLClient::is_valid() const { return ctx_ != nullptr; } void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) { shutdown_ssl_impl(socket, shutdown_gracefully); } void SSLClient::shutdown_ssl_impl(Socket &socket, bool shutdown_gracefully) { if (socket.sock == INVALID_SOCKET) { assert(socket.ssl == nullptr); return; } if (socket.ssl) { tls::shutdown(socket.ssl, shutdown_gracefully); { std::lock_guard guard(ctx_mutex_); tls::free_session(socket.ssl); } socket.ssl = nullptr; } assert(socket.ssl == nullptr); } bool SSLClient::process_socket( const Socket &socket, std::chrono::time_point start_time, std::function callback) { assert(socket.ssl); return detail::process_client_socket_ssl( socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, std::move(callback)); } bool SSLClient::is_ssl() const { return true; } bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) { if (!is_valid()) { error = Error::SSLConnection; return false; } return ClientImpl::create_and_connect_socket(socket, error); } bool SSLClient::setup_proxy_connection( Socket &socket, std::chrono::time_point start_time, Response &res, bool &success, Error &error) { if (!is_proxy_enabled_for_host(host_)) { return true; } if (!connect_with_proxy(socket, start_time, res, success, error)) { return false; } if (!initialize_ssl(socket, error)) { success = false; return false; } return true; } // Assumes that socket_mutex_ is locked and that there are no requests in // flight bool SSLClient::connect_with_proxy( Socket &socket, std::chrono::time_point start_time, Response &res, bool &success, Error &error) { success = true; Response proxy_res; if (!detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, [&](Stream &strm) { Request req2; req2.method = "CONNECT"; req2.path = detail::make_host_and_port_string_always_port(host_, port_); if (max_timeout_msec_ > 0) { req2.start_time_ = std::chrono::steady_clock::now(); } return process_request(strm, req2, proxy_res, false, error); })) { // Thread-safe to close everything because we are assuming there are no // requests in flight shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); success = false; return false; } if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407) { if (!proxy_digest_auth_username_.empty() && !proxy_digest_auth_password_.empty()) { std::map auth; if (detail::parse_www_authenticate(proxy_res, auth, true)) { // Close the current socket and create a new one for the authenticated // request shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); // Create a new socket for the authenticated CONNECT request if (!ensure_socket_connection(socket, error)) { success = false; output_error_log(error, nullptr); return false; } proxy_res = Response(); if (!detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, [&](Stream &strm) { Request req3; req3.method = "CONNECT"; req3.path = detail::make_host_and_port_string_always_port( host_, port_); req3.headers.insert(detail::make_digest_authentication_header( req3, auth, 1, detail::random_string(10), proxy_digest_auth_username_, proxy_digest_auth_password_, true)); if (max_timeout_msec_ > 0) { req3.start_time_ = std::chrono::steady_clock::now(); } return process_request(strm, req3, proxy_res, false, error); })) { // Thread-safe to close everything because we are assuming there are // no requests in flight shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); success = false; return false; } } } } // If status code is not 200, proxy request is failed. // Set error to ProxyConnection and return proxy response // as the response of the request if (proxy_res.status != StatusCode::OK_200) { error = Error::ProxyConnection; output_error_log(error, nullptr); res = std::move(proxy_res); // Thread-safe to close everything because we are assuming there are // no requests in flight shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); return false; } return true; } bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) { if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; } if (is_proxy_enabled_for_host(host_)) { return true; } if (!initialize_ssl(socket, error)) { shutdown_socket(socket); close_socket(socket); return false; } return true; } // SSL HTTP client implementation SSLClient::SSLClient(const std::string &host) : SSLClient(host, 443, std::string(), std::string()) {} SSLClient::SSLClient(const std::string &host, int port) : SSLClient(host, port, std::string(), std::string()) {} void SSLClient::init_ctx() { ctx_ = tls::create_client_context(); if (ctx_) { tls::set_min_version(ctx_, tls::Version::TLS1_2); } } void SSLClient::reset_ctx_on_error() { last_backend_error_ = tls::get_error(); tls::free_context(ctx_); ctx_ = nullptr; } SSLClient::SSLClient(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path, const std::string &private_key_password) : ClientImpl(host, port, client_cert_path, client_key_path) { init_ctx(); if (!ctx_) { return; } if (!client_cert_path.empty() && !client_key_path.empty()) { const char *password = private_key_password.empty() ? nullptr : private_key_password.c_str(); if (!tls::set_client_cert_file(ctx_, client_cert_path.c_str(), client_key_path.c_str(), password)) { reset_ctx_on_error(); } } } SSLClient::SSLClient(const std::string &host, int port, const PemMemory &pem) : ClientImpl(host, port) { init_ctx(); if (!ctx_) { return; } if (pem.cert_pem && pem.key_pem) { if (!tls::set_client_cert_pem(ctx_, pem.cert_pem, pem.key_pem, pem.private_key_password)) { reset_ctx_on_error(); } } } void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) { if (ca_cert_store && ctx_) { // set_ca_store takes ownership of ca_cert_store tls::set_ca_store(ctx_, ca_cert_store); ca_cert_store_set_ = true; } else if (ca_cert_store) { tls::free_ca_store(ca_cert_store); } } void SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) { if (!ctx_) { return; } tls::set_verify_callback(ctx_, verifier); } void SSLClient::set_session_verifier( std::function verifier) { session_verifier_ = std::move(verifier); } #ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE void SSLClient::enable_windows_certificate_verification(bool enabled) { enable_windows_cert_verification_ = enabled; } #endif void SSLClient::load_ca_cert_store(const char *ca_cert, std::size_t size) { if (ctx_ && ca_cert && size > 0) { ca_cert_pem_.assign(ca_cert, size); // Store for redirect transfer tls::load_ca_pem(ctx_, ca_cert, size); } } bool SSLClient::load_certs() { auto ret = true; std::call_once(initialize_cert_, [&]() { std::lock_guard guard(ctx_mutex_); ret = detail::load_client_ca_config( ctx_, ca_cert_file_path_, ca_cert_dir_path_, !ca_cert_pem_.empty() || ca_cert_store_set_, system_ca_mode_, last_backend_error_); }); return ret; } 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()) { error = Error::SSLLoadingCerts; output_error_log(error, nullptr); return false; } } 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_); #endif // Create TLS session session_t session = nullptr; { std::lock_guard guard(ctx_mutex_); session = create_session(ctx_, socket.sock); } if (!session) { error = Error::SSLConnection; last_backend_error_ = get_error(); return false; } // 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); } }); // 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(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; 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(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 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; } void Client::set_digest_auth(const std::string &username, const std::string &password) { cli_->set_digest_auth(username, password); } void Client::set_proxy_digest_auth(const std::string &username, const std::string &password) { cli_->set_proxy_digest_auth(username, password); } void Client::enable_server_certificate_verification(bool enabled) { cli_->enable_server_certificate_verification(enabled); } void Client::enable_server_hostname_verification(bool enabled) { cli_->enable_server_hostname_verification(enabled); } void Client::enable_system_ca(bool enabled) { cli_->enable_system_ca(enabled); } #ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE void Client::enable_windows_certificate_verification(bool enabled) { if (is_ssl_) { static_cast(*cli_).enable_windows_certificate_verification( enabled); } } #endif void Client::set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path) { cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path); } void Client::set_ca_cert_store(tls::ca_store_t ca_cert_store) { if (is_ssl_) { static_cast(*cli_).set_ca_cert_store(ca_cert_store); } else if (ca_cert_store) { tls::free_ca_store(ca_cert_store); } } void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) { if (is_ssl_) { // Use the PEM-based path so the CA data is retained for redirect transfer static_cast(*cli_).load_ca_cert_store(ca_cert, size); } } void Client::set_server_certificate_verifier(tls::VerifyCallback verifier) { if (is_ssl_) { static_cast(*cli_).set_server_certificate_verifier( std::move(verifier)); } } void Client::set_session_verifier( std::function verifier) { if (is_ssl_) { static_cast(*cli_).set_session_verifier(std::move(verifier)); } } tls::ctx_t Client::tls_context() const { if (is_ssl_) { return static_cast(*cli_).tls_context(); } return nullptr; } #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 7: TLS abstraction layer - Common API */ #ifdef CPPHTTPLIB_SSL_ENABLED namespace tls { // Helper for PeerCert construction PeerCert get_peer_cert_from_session(const_session_t session) { return PeerCert(get_peer_cert(session)); } namespace impl { VerifyCallback &get_verify_callback() { static thread_local VerifyCallback callback; return callback; } VerifyCallback &get_mbedtls_verify_callback() { static thread_local VerifyCallback callback; return callback; } // Check if a string is an IPv4 address bool is_ipv4_address(const std::string &str) { int dots = 0; for (char c : str) { if (c == '.') { dots++; } else if (!isdigit(static_cast(c))) { return false; } } return dots == 3; } // Parse IPv4 address string to bytes bool parse_ipv4(const std::string &str, unsigned char *out) { const char *p = str.c_str(); for (int i = 0; i < 4; i++) { if (i > 0) { if (*p != '.') { return false; } p++; } int val = 0; int digits = 0; while (*p >= '0' && *p <= '9') { val = val * 10 + (*p - '0'); if (val > 255) { return false; } p++; digits++; } if (digits == 0) { return false; } // Reject leading zeros (e.g., "01.002.03.04") to prevent ambiguity if (digits > 1 && *(p - digits) == '0') { return false; } out[i] = static_cast(val); } return *p == '\0'; } // Parse an IP literal (IPv4 or IPv6) into raw network-order bytes. // `out` must have room for at least 16 bytes. Returns the address length // (4 for IPv4, 16 for IPv6) on success, or 0 if the string is not an IP // literal. Used to match a host against iPAddress SANs the same way the // OpenSSL backend does via X509_check_ip. size_t parse_ip_address(const std::string &str, unsigned char *out) { if (is_ipv4_address(str)) { return parse_ipv4(str, out) ? 4 : 0; } struct in6_addr addr6 = {}; if (inet_pton(AF_INET6, str.c_str(), &addr6) == 1) { memcpy(out, &addr6, 16); return 16; } return 0; } #ifdef _WIN32 // Enumerate Windows system certificates and call callback with DER data template bool enumerate_windows_system_certs(Callback cb) { bool loaded = false; static const wchar_t *store_names[] = {L"ROOT", L"CA"}; for (auto store_name : store_names) { HCERTSTORE hStore = CertOpenSystemStoreW(0, store_name); if (hStore) { PCCERT_CONTEXT pContext = nullptr; while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != nullptr) { if (cb(pContext->pbCertEncoded, pContext->cbCertEncoded)) { loaded = true; } } CertCloseStore(hStore, 0); } } return loaded; } #endif #ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN // Enumerate macOS Keychain certificates and call callback with DER data template bool enumerate_macos_keychain_certs(Callback cb) { bool loaded = false; const SecTrustSettingsDomain domains[] = { kSecTrustSettingsDomainSystem, kSecTrustSettingsDomainAdmin, kSecTrustSettingsDomainUser, }; for (auto domain : domains) { CFArrayRef certs = nullptr; OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs); if (status != errSecSuccess || !certs) { if (certs) CFRelease(certs); continue; } CFIndex count = CFArrayGetCount(certs); for (CFIndex i = 0; i < count; i++) { SecCertificateRef cert = (SecCertificateRef)CFArrayGetValueAtIndex(certs, i); CFDataRef data = SecCertificateCopyData(cert); if (data) { if (cb(CFDataGetBytePtr(data), static_cast(CFDataGetLength(data)))) { loaded = true; } CFRelease(data); } } CFRelease(certs); } return loaded; } #endif #if !defined(_WIN32) && !(defined(__APPLE__) && \ defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)) // Common CA certificate file paths on Linux/Unix const char **system_ca_paths() { static const char *paths[] = { "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu "/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS "/etc/ssl/ca-bundle.pem", // OpenSUSE "/etc/pki/tls/cacert.pem", // OpenELEC "/etc/ssl/cert.pem", // Alpine, FreeBSD nullptr}; return paths; } // Common CA certificate directory paths on Linux/Unix const char **system_ca_dirs() { static const char *dirs[] = {"/etc/ssl/certs", // Debian/Ubuntu "/etc/pki/tls/certs", // RHEL/CentOS "/usr/share/ca-certificates", // Other nullptr}; return dirs; } #endif } // namespace impl bool set_client_ca_file(ctx_t ctx, const char *ca_file, const char *ca_dir) { if (!ctx) { return false; } bool success = true; if (ca_file && *ca_file) { if (!load_ca_file(ctx, ca_file)) { success = false; } } if (ca_dir && *ca_dir) { if (!load_ca_dir(ctx, ca_dir)) { success = false; } } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // Set CA list for client certificate request (CertificateRequest message) if (ca_file && *ca_file) { auto list = SSL_load_client_CA_file(ca_file); if (list) { SSL_CTX_set_client_CA_list(static_cast(ctx), list); } } #endif return success; } bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password) { return set_client_cert_pem(ctx, cert, key, password); } bool set_server_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password) { return set_client_cert_file(ctx, cert_path, key_path, password); } // PeerCert implementation PeerCert::PeerCert() = default; PeerCert::PeerCert(cert_t cert) : cert_(cert) {} PeerCert::PeerCert(PeerCert &&other) noexcept : cert_(other.cert_) { other.cert_ = nullptr; } PeerCert &PeerCert::operator=(PeerCert &&other) noexcept { if (this != &other) { if (cert_) { free_cert(cert_); } cert_ = other.cert_; other.cert_ = nullptr; } return *this; } PeerCert::~PeerCert() { if (cert_) { free_cert(cert_); } } PeerCert::operator bool() const { return cert_ != nullptr; } std::string PeerCert::subject_cn() const { return cert_ ? get_cert_subject_cn(cert_) : std::string(); } std::string PeerCert::issuer_name() const { return cert_ ? get_cert_issuer_name(cert_) : std::string(); } bool PeerCert::check_hostname(const char *hostname) const { return cert_ ? verify_hostname(cert_, hostname) : false; } std::vector PeerCert::sans() const { std::vector result; if (cert_) { get_cert_sans(cert_, result); } return result; } bool PeerCert::validity(time_t ¬_before, time_t ¬_after) const { return cert_ ? get_cert_validity(cert_, not_before, not_after) : false; } std::string PeerCert::serial() const { return cert_ ? get_cert_serial(cert_) : std::string(); } // VerifyContext method implementations std::string VerifyContext::subject_cn() const { return cert ? get_cert_subject_cn(cert) : std::string(); } std::string VerifyContext::issuer_name() const { return cert ? get_cert_issuer_name(cert) : std::string(); } bool VerifyContext::check_hostname(const char *hostname) const { return cert ? verify_hostname(cert, hostname) : false; } std::vector VerifyContext::sans() const { std::vector result; if (cert) { get_cert_sans(cert, result); } return result; } bool VerifyContext::validity(time_t ¬_before, time_t ¬_after) const { return cert ? get_cert_validity(cert, not_before, not_after) : false; } std::string VerifyContext::serial() const { return cert ? get_cert_serial(cert) : std::string(); } // TlsError static method implementation std::string TlsError::verify_error_to_string(long error_code) { return verify_error_string(error_code); } } // namespace tls // Request::peer_cert() implementation tls::PeerCert Request::peer_cert() const { return tls::get_peer_cert_from_session(ssl); } // Request::sni() implementation std::string Request::sni() const { if (!ssl) { return std::string(); } const char *s = tls::get_sni(ssl); return s ? std::string(s) : std::string(); } #endif // CPPHTTPLIB_SSL_ENABLED /* * Group 8: TLS abstraction layer - OpenSSL backend */ /* * OpenSSL Backend Implementation */ #ifdef CPPHTTPLIB_OPENSSL_SUPPORT namespace tls { namespace impl { // Helper to map OpenSSL SSL_get_error to ErrorCode ErrorCode map_ssl_error(int ssl_error, int &out_errno) { switch (ssl_error) { case SSL_ERROR_NONE: return ErrorCode::Success; case SSL_ERROR_WANT_READ: return ErrorCode::WantRead; case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite; case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed; case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError; case SSL_ERROR_SSL: default: return ErrorCode::Fatal; } } // 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 STACK_OF(X509_NAME) * create_client_ca_list_from_pem(const char *ca_pem) { if (!ca_pem) { return nullptr; } auto ca_list = sk_X509_NAME_new_null(); if (!ca_list) { return nullptr; } BIO *bio = BIO_new_mem_buf(ca_pem, -1); if (!bio) { sk_X509_NAME_pop_free(ca_list, X509_NAME_free); return nullptr; } X509 *cert = nullptr; while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != nullptr) { const X509_NAME *name = X509_get_subject_name(cert); if (name) { sk_X509_NAME_push(ca_list, X509_NAME_dup(const_cast(name))); } X509_free(cert); } BIO_free(bio); return ca_list; } // OpenSSL verify callback wrapper int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { auto &callback = get_verify_callback(); if (!callback) { return preverify_ok; } // Get SSL object from X509_STORE_CTX auto ssl = static_cast( X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); if (!ssl) { return preverify_ok; } // Get current certificate and depth auto cert = X509_STORE_CTX_get_current_cert(ctx); int depth = X509_STORE_CTX_get_error_depth(ctx); int error = X509_STORE_CTX_get_error(ctx); // Build context VerifyContext verify_ctx; verify_ctx.session = static_cast(ssl); verify_ctx.cert = static_cast(cert); verify_ctx.depth = depth; verify_ctx.preverify_ok = (preverify_ok != 0); verify_ctx.error_code = error; verify_ctx.error_string = (error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr; return callback(verify_ctx) ? 1 : 0; } // X509_STORE_get0_objects is deprecated since OpenSSL 4.0 because it is not // thread-safe; X509_STORE_get1_objects (OpenSSL 3.3+) returns a snapshot // that must be released with release_store_objects #if !defined(OPENSSL_IS_BORINGSSL) && !defined(LIBRESSL_VERSION_NUMBER) && \ OPENSSL_VERSION_NUMBER >= 0x30300000L #define CPPHTTPLIB_HAS_X509_STORE_GET1_OBJECTS #endif STACK_OF(X509_OBJECT) * get_store_objects(X509_STORE *store) { #ifdef CPPHTTPLIB_HAS_X509_STORE_GET1_OBJECTS return X509_STORE_get1_objects(store); #else return X509_STORE_get0_objects(store); #endif } void release_store_objects(STACK_OF(X509_OBJECT) * objs) { #ifdef CPPHTTPLIB_HAS_X509_STORE_GET1_OBJECTS sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free); #else (void)objs; // get0 variant returns an internal pointer; nothing to free #endif } } // namespace impl ctx_t create_client_context() { SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); if (ctx) { // Disable auto-retry to properly handle non-blocking I/O SSL_CTX_clear_mode(ctx, SSL_MODE_AUTO_RETRY); // Set minimum TLS version SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); } return static_cast(ctx); } void free_context(ctx_t ctx) { if (ctx) { SSL_CTX_free(static_cast(ctx)); } } bool set_min_version(ctx_t ctx, Version version) { if (!ctx) return false; return SSL_CTX_set_min_proto_version(static_cast(ctx), static_cast(version)) == 1; } bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) { if (!ctx || !pem || len == 0) return false; auto ssl_ctx = static_cast(ctx); auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) return false; auto bio = BIO_new_mem_buf(pem, static_cast(len)); if (!bio) return false; bool ok = true; X509 *cert = nullptr; while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != nullptr) { if (X509_STORE_add_cert(store, cert) != 1) { // Ignore duplicate errors auto err = ERR_peek_last_error(); if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { ok = false; } } X509_free(cert); if (!ok) break; } BIO_free(bio); // Clear any "no more certificates" errors ERR_clear_error(); return ok; } bool load_ca_file(ctx_t ctx, const char *file_path) { if (!ctx || !file_path) return false; return SSL_CTX_load_verify_locations(static_cast(ctx), file_path, nullptr) == 1; } bool load_ca_dir(ctx_t ctx, const char *dir_path) { if (!ctx || !dir_path) return false; return SSL_CTX_load_verify_locations(static_cast(ctx), nullptr, dir_path) == 1; } bool load_system_certs(ctx_t ctx) { if (!ctx) return false; auto ssl_ctx = static_cast(ctx); #ifdef _WIN32 // Windows: Load from system certificate store (ROOT and CA) auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) return false; bool loaded_any = false; static const wchar_t *store_names[] = {L"ROOT", L"CA"}; for (auto store_name : store_names) { auto hStore = CertOpenSystemStoreW(NULL, store_name); if (!hStore) continue; PCCERT_CONTEXT pContext = nullptr; while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != nullptr) { const unsigned char *data = pContext->pbCertEncoded; auto x509 = d2i_X509(nullptr, &data, pContext->cbCertEncoded); if (x509) { if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } X509_free(x509); } } CertCloseStore(hStore, 0); } return loaded_any; #elif defined(__APPLE__) #ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN // macOS: Load from Keychain auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) return false; bool loaded_any = false; const SecTrustSettingsDomain domains[] = { kSecTrustSettingsDomainSystem, kSecTrustSettingsDomainAdmin, kSecTrustSettingsDomainUser, }; for (auto domain : domains) { CFArrayRef certs = nullptr; if (SecTrustSettingsCopyCertificates(domain, &certs) != errSecSuccess || !certs) { if (certs) CFRelease(certs); continue; } auto count = CFArrayGetCount(certs); for (CFIndex i = 0; i < count; i++) { auto cert = reinterpret_cast( const_cast(CFArrayGetValueAtIndex(certs, i))); CFDataRef der = SecCertificateCopyData(cert); if (der) { const unsigned char *data = CFDataGetBytePtr(der); auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der)); if (x509) { if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } X509_free(x509); } CFRelease(der); } } CFRelease(certs); } return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; #else return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; #endif #else // Other Unix: use default verify paths return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; #endif } bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password) { if (!ctx || !cert || !key) return false; auto ssl_ctx = static_cast(ctx); // Load certificate auto cert_bio = BIO_new_mem_buf(cert, -1); if (!cert_bio) return false; auto x509 = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); BIO_free(cert_bio); if (!x509) return false; auto cert_ok = SSL_CTX_use_certificate(ssl_ctx, x509) == 1; X509_free(x509); if (!cert_ok) return false; // Load private key auto key_bio = BIO_new_mem_buf(key, -1); if (!key_bio) return false; auto pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, password ? const_cast(password) : nullptr); BIO_free(key_bio); if (!pkey) return false; auto key_ok = SSL_CTX_use_PrivateKey(ssl_ctx, pkey) == 1; EVP_PKEY_free(pkey); return key_ok && SSL_CTX_check_private_key(ssl_ctx) == 1; } bool set_client_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password) { if (!ctx || !cert_path || !key_path) return false; auto ssl_ctx = static_cast(ctx); if (password && password[0] != '\0') { SSL_CTX_set_default_passwd_cb_userdata( ssl_ctx, reinterpret_cast(const_cast(password))); } return SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_path) == 1 && SSL_CTX_use_PrivateKey_file(ssl_ctx, key_path, SSL_FILETYPE_PEM) == 1; } ctx_t create_server_context() { SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); if (ctx) { SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); } return static_cast(ctx); } void set_verify_client(ctx_t ctx, bool require) { if (!ctx) return; SSL_CTX_set_verify(static_cast(ctx), require ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) : SSL_VERIFY_NONE, nullptr); } session_t create_session(ctx_t ctx, socket_t sock) { if (!ctx || sock == INVALID_SOCKET) return nullptr; auto ssl_ctx = static_cast(ctx); SSL *ssl = SSL_new(ssl_ctx); if (!ssl) return nullptr; // Disable auto-retry for proper non-blocking I/O handling SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); if (!bio) { SSL_free(ssl); return nullptr; } SSL_set_bio(ssl, bio, bio); return static_cast(ssl); } void free_session(session_t session) { if (session) { SSL_free(static_cast(session)); } } bool set_sni(session_t session, const char *hostname) { if (!session || !hostname) return false; auto ssl = static_cast(session); // Set SNI (Server Name Indication) only - does not enable verification #if defined(OPENSSL_IS_BORINGSSL) return SSL_set_tlsext_host_name(ssl, hostname) == 1; #else // Direct call instead of macro to suppress -Wold-style-cast warning return SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, static_cast(const_cast(hostname))) == 1; #endif } bool set_hostname(session_t session, const char *hostname) { if (!session || !hostname) return false; auto ssl = static_cast(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(); } auto ssl = static_cast(session); auto ret = SSL_connect(ssl); TlsError err; 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(); } return err; } TlsError accept(session_t session) { if (!session) { return TlsError(); } auto ssl = static_cast(session); auto ret = SSL_accept(ssl); TlsError err; 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(); } return err; } bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { if (!session) { if (err) { err->code = ErrorCode::Fatal; } return false; } auto ssl = static_cast(session); auto bio = SSL_get_rbio(ssl); // Set non-blocking mode for handshake detail::set_nonblocking(sock, true); if (bio) { BIO_set_nbio(bio, 1); } auto cleanup = detail::scope_exit([&]() { // Restore blocking mode after handshake if (bio) { BIO_set_nbio(bio, 0); } detail::set_nonblocking(sock, false); }); auto res = 0; while ((res = SSL_connect(ssl)) != 1) { auto ssl_err = SSL_get_error(ssl, res); switch (ssl_err) { case SSL_ERROR_WANT_READ: if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } break; case SSL_ERROR_WANT_WRITE: if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } break; default: break; } if (err) { err->code = impl::map_ssl_error(ssl_err, err->sys_errno); err->backend_code = ERR_get_error(); } return false; } if (err) { err->code = ErrorCode::Success; } return true; } bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { if (!session) { if (err) { err->code = ErrorCode::Fatal; } return false; } auto ssl = static_cast(session); auto bio = SSL_get_rbio(ssl); // Set non-blocking mode for handshake detail::set_nonblocking(sock, true); if (bio) { BIO_set_nbio(bio, 1); } auto cleanup = detail::scope_exit([&]() { // Restore blocking mode after handshake if (bio) { BIO_set_nbio(bio, 0); } detail::set_nonblocking(sock, false); }); auto res = 0; while ((res = SSL_accept(ssl)) != 1) { auto ssl_err = SSL_get_error(ssl, res); switch (ssl_err) { case SSL_ERROR_WANT_READ: if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } break; case SSL_ERROR_WANT_WRITE: if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } break; default: break; } if (err) { err->code = impl::map_ssl_error(ssl_err, err->sys_errno); err->backend_code = ERR_get_error(); } return false; } if (err) { err->code = ErrorCode::Success; } return true; } ssize_t read(session_t session, void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto ssl = static_cast(session); constexpr auto max_len = static_cast((std::numeric_limits::max)()); if (len > max_len) { len = max_len; } auto ret = SSL_read(ssl, buf, static_cast(len)); if (ret > 0) { err.code = ErrorCode::Success; return ret; } auto ssl_err = SSL_get_error(ssl, ret); err.code = impl::map_ssl_error(ssl_err, err.sys_errno); if (err.code == ErrorCode::PeerClosed) { return 0; } // Gracefully handle the peer closed state. if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } return -1; } ssize_t write(session_t session, const void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto ssl = static_cast(session); auto ret = SSL_write(ssl, buf, static_cast(len)); if (ret > 0) { err.code = ErrorCode::Success; return ret; } auto ssl_err = SSL_get_error(ssl, ret); err.code = impl::map_ssl_error(ssl_err, err.sys_errno); if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } return -1; } int pending(const_session_t session) { if (!session) return 0; return SSL_pending(static_cast(const_cast(session))); } void shutdown(session_t session, bool graceful) { if (!session) return; auto ssl = static_cast(session); if (graceful) { // First call sends close_notify if (SSL_shutdown(ssl) == 0) { // Second call waits for peer's close_notify SSL_shutdown(ssl); } } } bool is_peer_closed(session_t session, socket_t sock) { if (!session) return true; // Temporarily set socket to non-blocking to avoid blocking on SSL_peek detail::set_nonblocking(sock, true); auto se = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); auto ssl = static_cast(session); char buf; auto ret = SSL_peek(ssl, &buf, 1); if (ret > 0) return false; auto err = SSL_get_error(ssl, ret); return err == SSL_ERROR_ZERO_RETURN; } cert_t get_peer_cert(const_session_t session) { if (!session) return nullptr; return static_cast(SSL_get1_peer_certificate( static_cast(const_cast(session)))); } void free_cert(cert_t cert) { if (cert) { X509_free(static_cast(cert)); } } bool verify_hostname(cert_t cert, const char *hostname) { if (!cert || !hostname) return false; auto x509 = static_cast(cert); // Use X509_check_ip_asc for IP addresses, X509_check_host for DNS names if (detail::is_ip_address(hostname)) { return X509_check_ip_asc(x509, hostname, 0) == 1; } return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1; } uint64_t hostname_mismatch_code() { return static_cast(X509_V_ERR_HOSTNAME_MISMATCH); } long get_verify_result(const_session_t session) { if (!session) return X509_V_ERR_UNSPECIFIED; return SSL_get_verify_result(static_cast(const_cast(session))); } std::string get_cert_subject_cn(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); auto subject_name = X509_get_subject_name(x509); if (!subject_name) return ""; // X509_NAME_get_text_by_NID is deprecated since OpenSSL 4.0 auto idx = X509_NAME_get_index_by_NID(subject_name, NID_commonName, -1); if (idx < 0) return ""; auto entry = X509_NAME_get_entry(subject_name, idx); if (!entry) return ""; auto data = X509_NAME_ENTRY_get_data(entry); if (!data) return ""; return std::string( reinterpret_cast(ASN1_STRING_get0_data(data)), static_cast(ASN1_STRING_length(data))); } std::string get_cert_issuer_name(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); auto issuer_name = X509_get_issuer_name(x509); if (!issuer_name) return ""; char buf[256]; X509_NAME_oneline(issuer_name, buf, sizeof(buf)); return std::string(buf); } bool get_cert_sans(cert_t cert, std::vector &sans) { sans.clear(); if (!cert) return false; auto x509 = static_cast(cert); auto names = static_cast( X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr)); if (!names) return true; // No SANs is valid auto count = sk_GENERAL_NAME_num(names); for (decltype(count) i = 0; i < count; i++) { auto gen = sk_GENERAL_NAME_value(names, i); if (!gen) continue; SanEntry entry; switch (gen->type) { case GEN_DNS: entry.type = SanType::DNS; if (gen->d.dNSName) { entry.value = std::string( reinterpret_cast( ASN1_STRING_get0_data(gen->d.dNSName)), static_cast(ASN1_STRING_length(gen->d.dNSName))); } break; case GEN_IPADD: entry.type = SanType::IP; if (gen->d.iPAddress) { auto data = ASN1_STRING_get0_data(gen->d.iPAddress); auto len = ASN1_STRING_length(gen->d.iPAddress); if (len == 4) { // IPv4 char buf[INET_ADDRSTRLEN]; inet_ntop(AF_INET, data, buf, sizeof(buf)); entry.value = buf; } else if (len == 16) { // IPv6 char buf[INET6_ADDRSTRLEN]; inet_ntop(AF_INET6, data, buf, sizeof(buf)); entry.value = buf; } } break; case GEN_EMAIL: entry.type = SanType::EMAIL; if (gen->d.rfc822Name) { entry.value = std::string( reinterpret_cast( ASN1_STRING_get0_data(gen->d.rfc822Name)), static_cast(ASN1_STRING_length(gen->d.rfc822Name))); } break; case GEN_URI: entry.type = SanType::URI; if (gen->d.uniformResourceIdentifier) { entry.value = std::string( reinterpret_cast( ASN1_STRING_get0_data(gen->d.uniformResourceIdentifier)), static_cast( ASN1_STRING_length(gen->d.uniformResourceIdentifier))); } break; default: entry.type = SanType::OTHER; break; } if (!entry.value.empty()) { sans.push_back(std::move(entry)); } } GENERAL_NAMES_free(names); return true; } bool get_cert_validity(cert_t cert, time_t ¬_before, time_t ¬_after) { if (!cert) return false; auto x509 = static_cast(cert); auto nb = X509_get0_notBefore(x509); auto na = X509_get0_notAfter(x509); if (!nb || !na) return false; ASN1_TIME *epoch = ASN1_TIME_new(); if (!epoch) return false; auto se = detail::scope_exit([&] { ASN1_TIME_free(epoch); }); if (!ASN1_TIME_set(epoch, 0)) return false; int pday, psec; if (!ASN1_TIME_diff(&pday, &psec, epoch, nb)) return false; not_before = 86400 * (time_t)pday + psec; if (!ASN1_TIME_diff(&pday, &psec, epoch, na)) return false; not_after = 86400 * (time_t)pday + psec; return true; } std::string get_cert_serial(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); auto serial = X509_get_serialNumber(x509); if (!serial) return ""; auto bn = ASN1_INTEGER_to_BN(serial, nullptr); if (!bn) return ""; auto hex = BN_bn2hex(bn); BN_free(bn); if (!hex) return ""; std::string result(hex); OPENSSL_free(hex); return result; } bool get_cert_der(cert_t cert, std::vector &der) { if (!cert) return false; auto x509 = static_cast(cert); auto len = i2d_X509(x509, nullptr); if (len < 0) return false; der.resize(static_cast(len)); auto p = der.data(); i2d_X509(x509, &p); return true; } const char *get_sni(const_session_t session) { if (!session) return nullptr; auto ssl = static_cast(const_cast(session)); return SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); } uint64_t peek_error() { return ERR_peek_last_error(); } uint64_t get_error() { return ERR_get_error(); } std::string error_string(uint64_t code) { char buf[256]; ERR_error_string_n(static_cast(code), buf, sizeof(buf)); return std::string(buf); } ca_store_t create_ca_store(const char *pem, size_t len) { auto mem = BIO_new_mem_buf(pem, static_cast(len)); if (!mem) { return nullptr; } auto mem_guard = detail::scope_exit([&] { BIO_free_all(mem); }); auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr); if (!inf) { return nullptr; } auto store = X509_STORE_new(); if (store) { for (auto i = 0; i < static_cast(sk_X509_INFO_num(inf)); i++) { auto itmp = sk_X509_INFO_value(inf, i); if (!itmp) { continue; } if (itmp->x509) { X509_STORE_add_cert(store, itmp->x509); } if (itmp->crl) { X509_STORE_add_crl(store, itmp->crl); } } } sk_X509_INFO_pop_free(inf, X509_INFO_free); return static_cast(store); } void free_ca_store(ca_store_t store) { if (store) { X509_STORE_free(static_cast(store)); } } bool set_ca_store(ctx_t ctx, ca_store_t store) { if (!ctx || !store) { return false; } auto ssl_ctx = static_cast(ctx); auto x509_store = static_cast(store); // Check if same store is already set if (SSL_CTX_get_cert_store(ssl_ctx) == x509_store) { return true; } // SSL_CTX_set_cert_store takes ownership and frees the old store SSL_CTX_set_cert_store(ssl_ctx, x509_store); return true; } size_t get_ca_certs(ctx_t ctx, std::vector &certs) { certs.clear(); if (!ctx) { return 0; } auto ssl_ctx = static_cast(ctx); auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) { return 0; } auto objs = impl::get_store_objects(store); if (!objs) { return 0; } auto se = detail::scope_exit([&] { impl::release_store_objects(objs); }); auto count = sk_X509_OBJECT_num(objs); for (decltype(count) i = 0; i < count; i++) { auto obj = sk_X509_OBJECT_value(objs, i); if (!obj) { continue; } if (X509_OBJECT_get_type(obj) == X509_LU_X509) { auto x509 = X509_OBJECT_get0_X509(obj); if (x509) { // Increment reference count so caller can free it X509_up_ref(x509); certs.push_back(static_cast(x509)); } } } return certs.size(); } std::vector get_ca_names(ctx_t ctx) { std::vector names; if (!ctx) { return names; } auto ssl_ctx = static_cast(ctx); auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) { return names; } auto objs = impl::get_store_objects(store); if (!objs) { return names; } auto se = detail::scope_exit([&] { impl::release_store_objects(objs); }); auto count = sk_X509_OBJECT_num(objs); for (decltype(count) i = 0; i < count; i++) { auto obj = sk_X509_OBJECT_value(objs, i); if (!obj) { continue; } if (X509_OBJECT_get_type(obj) == X509_LU_X509) { auto x509 = X509_OBJECT_get0_X509(obj); if (x509) { auto subject = X509_get_subject_name(x509); if (subject) { char buf[512]; X509_NAME_oneline(subject, buf, sizeof(buf)); names.push_back(buf); } } } } return names; } bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem, const char *password) { if (!ctx || !cert_pem || !key_pem) { return false; } auto ssl_ctx = static_cast(ctx); // Load certificate from PEM auto cert_bio = BIO_new_mem_buf(cert_pem, -1); if (!cert_bio) { return false; } auto cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); BIO_free(cert_bio); if (!cert) { return false; } // Load private key from PEM auto key_bio = BIO_new_mem_buf(key_pem, -1); if (!key_bio) { X509_free(cert); return false; } auto key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, password ? const_cast(password) : nullptr); BIO_free(key_bio); if (!key) { X509_free(cert); return false; } // Update certificate and key auto ret = SSL_CTX_use_certificate(ssl_ctx, cert) == 1 && SSL_CTX_use_PrivateKey(ssl_ctx, key) == 1; X509_free(cert); EVP_PKEY_free(key); return ret; } bool update_server_client_ca(ctx_t ctx, const char *ca_pem) { if (!ctx || !ca_pem) { return false; } auto ssl_ctx = static_cast(ctx); // Create new X509_STORE from PEM auto store = create_ca_store(ca_pem, strlen(ca_pem)); if (!store) { return false; } // SSL_CTX_set_cert_store takes ownership SSL_CTX_set_cert_store(ssl_ctx, static_cast(store)); // Set client CA list for client certificate request auto ca_list = impl::create_client_ca_list_from_pem(ca_pem); if (ca_list) { // SSL_CTX_set_client_CA_list takes ownership of ca_list SSL_CTX_set_client_CA_list(ssl_ctx, ca_list); } return true; } bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (!ctx) { return false; } auto ssl_ctx = static_cast(ctx); impl::get_verify_callback() = std::move(callback); if (impl::get_verify_callback()) { SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, impl::openssl_verify_callback); } else { SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, nullptr); } return true; } long get_verify_error(const_session_t session) { if (!session) { return -1; } auto ssl = static_cast(const_cast(session)); return SSL_get_verify_result(ssl); } std::string verify_error_string(long error_code) { if (error_code == X509_V_OK) { return ""; } const char *str = X509_verify_cert_error_string(static_cast(error_code)); return str ? str : "unknown error"; } } // namespace tls #endif // CPPHTTPLIB_OPENSSL_SUPPORT /* * Group 9: TLS abstraction layer - Mbed TLS backend */ /* * Mbed TLS Backend Implementation */ #ifdef CPPHTTPLIB_MBEDTLS_SUPPORT namespace tls { namespace impl { // Mbed TLS session wrapper struct MbedTlsSession { mbedtls_ssl_context ssl; socket_t sock = INVALID_SOCKET; std::string hostname; // For client: set via set_sni std::string sni_hostname; // For server: received from client via SNI callback MbedTlsSession() { mbedtls_ssl_init(&ssl); } ~MbedTlsSession() { mbedtls_ssl_free(&ssl); } MbedTlsSession(const MbedTlsSession &) = delete; MbedTlsSession &operator=(const MbedTlsSession &) = delete; }; // Thread-local error code accessor for Mbed TLS (since it doesn't have an error // queue) int &mbedtls_last_error() { static thread_local int err = 0; return err; } // Helper to map Mbed TLS error to ErrorCode ErrorCode map_mbedtls_error(int ret, int &out_errno) { 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; } if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { return ErrorCode::PeerClosed; } if (ret == MBEDTLS_ERR_NET_CONN_RESET || ret == MBEDTLS_ERR_NET_SEND_FAILED || ret == MBEDTLS_ERR_NET_RECV_FAILED) { out_errno = errno; return ErrorCode::SyscallError; } if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { return ErrorCode::CertVerifyFailed; } return ErrorCode::Fatal; } // BIO-like send callback for Mbed TLS int mbedtls_net_send_cb(void *ctx, const unsigned char *buf, size_t len) { auto sock = *static_cast(ctx); #ifdef _WIN32 auto ret = send(sock, reinterpret_cast(buf), static_cast(len), 0); if (ret == SOCKET_ERROR) { int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_WRITE; } return MBEDTLS_ERR_NET_SEND_FAILED; } #else auto ret = send(sock, buf, len, 0); if (ret < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_WRITE; } return MBEDTLS_ERR_NET_SEND_FAILED; } #endif return static_cast(ret); } // BIO-like recv callback for Mbed TLS int mbedtls_net_recv_cb(void *ctx, unsigned char *buf, size_t len) { auto sock = *static_cast(ctx); #ifdef _WIN32 auto ret = recv(sock, reinterpret_cast(buf), static_cast(len), 0); if (ret == SOCKET_ERROR) { int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_READ; } return MBEDTLS_ERR_NET_RECV_FAILED; } #else auto ret = recv(sock, buf, len, 0); if (ret < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_READ; } return MBEDTLS_ERR_NET_RECV_FAILED; } #endif if (ret == 0) { return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; } return static_cast(ret); } // MbedTlsContext constructor/destructor implementations MbedTlsContext::MbedTlsContext() { mbedtls_ssl_config_init(&conf); mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&ctr_drbg); mbedtls_x509_crt_init(&ca_chain); mbedtls_x509_crt_init(&own_cert); mbedtls_pk_init(&own_key); } MbedTlsContext::~MbedTlsContext() { mbedtls_pk_free(&own_key); mbedtls_x509_crt_free(&own_cert); mbedtls_x509_crt_free(&ca_chain); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); mbedtls_ssl_config_free(&conf); } // Thread-local storage for SNI captured during handshake // This is needed because the SNI callback doesn't have a way to pass // session-specific data before the session is fully set up std::string &mbedpending_sni() { static thread_local std::string sni; return sni; } // SNI callback for Mbed TLS server to capture client's SNI hostname int mbedtls_sni_callback(void *p_ctx, mbedtls_ssl_context *ssl, const unsigned char *name, size_t name_len) { (void)p_ctx; (void)ssl; // Store SNI name in thread-local storage // It will be retrieved and stored in the session after handshake if (name && name_len > 0) { mbedpending_sni().assign(reinterpret_cast(name), name_len); } else { mbedpending_sni().clear(); } return 0; // Accept any SNI } 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(data); // Build context VerifyContext verify_ctx; verify_ctx.session = static_cast(session); verify_ctx.cert = static_cast(crt); verify_ctx.depth = cert_depth; verify_ctx.preverify_ok = (*flags == 0); verify_ctx.error_code = static_cast(*flags); // Convert Mbed TLS flags to error string static thread_local char error_buf[256]; if (*flags != 0) { mbedtls_x509_crt_verify_info(error_buf, sizeof(error_buf), "", *flags); verify_ctx.error_string = error_buf; } else { verify_ctx.error_string = nullptr; } bool accepted = callback(verify_ctx); if (accepted) { *flags = 0; // Clear all error flags return 0; } return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED; } } // namespace impl ctx_t create_client_context() { auto ctx = new (std::nothrow) impl::MbedTlsContext(); if (!ctx) { return nullptr; } ctx->is_server = false; // Seed the random number generator const char *pers = "httplib_client"; int ret = mbedtls_ctr_drbg_seed( &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy, reinterpret_cast(pers), strlen(pers)); if (ret != 0) { impl::mbedtls_last_error() = ret; delete ctx; return nullptr; } // Set up SSL config for client ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (ret != 0) { impl::mbedtls_last_error() = ret; delete ctx; return nullptr; } // Set random number generator mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg); // Default: verify peer certificate mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2); #else mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3); #endif return static_cast(ctx); } ctx_t create_server_context() { auto ctx = new (std::nothrow) impl::MbedTlsContext(); if (!ctx) { return nullptr; } ctx->is_server = true; // Seed the random number generator const char *pers = "httplib_server"; int ret = mbedtls_ctr_drbg_seed( &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy, reinterpret_cast(pers), strlen(pers)); if (ret != 0) { impl::mbedtls_last_error() = ret; delete ctx; return nullptr; } // Set up SSL config for server ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (ret != 0) { impl::mbedtls_last_error() = ret; delete ctx; return nullptr; } // Set random number generator mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg); // Default: don't verify client mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_NONE); // Set minimum TLS version to 1.2 #ifdef CPPHTTPLIB_MBEDTLS_V3 mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2); #else mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3); #endif // Set SNI callback to capture client's SNI hostname mbedtls_ssl_conf_sni(&ctx->conf, impl::mbedtls_sni_callback, nullptr); return static_cast(ctx); } void free_context(ctx_t ctx) { if (ctx) { delete static_cast(ctx); } } bool set_min_version(ctx_t ctx, Version version) { if (!ctx) { return false; } auto mctx = static_cast(ctx); #ifdef CPPHTTPLIB_MBEDTLS_V3 // Mbed TLS 3.x uses mbedtls_ssl_protocol_version enum mbedtls_ssl_protocol_version min_ver = MBEDTLS_SSL_VERSION_TLS1_2; if (version >= Version::TLS1_3) { #if defined(MBEDTLS_SSL_PROTO_TLS1_3) min_ver = MBEDTLS_SSL_VERSION_TLS1_3; #endif } mbedtls_ssl_conf_min_tls_version(&mctx->conf, min_ver); #else // Mbed TLS 2.x uses major/minor version numbers int major = MBEDTLS_SSL_MAJOR_VERSION_3; int minor = MBEDTLS_SSL_MINOR_VERSION_3; // TLS 1.2 if (version >= Version::TLS1_3) { #if defined(MBEDTLS_SSL_PROTO_TLS1_3) minor = MBEDTLS_SSL_MINOR_VERSION_4; // TLS 1.3 #else minor = MBEDTLS_SSL_MINOR_VERSION_3; // Fall back to TLS 1.2 #endif } mbedtls_ssl_conf_min_version(&mctx->conf, major, minor); #endif return true; } bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) { if (!ctx || !pem) { return false; } auto mctx = static_cast(ctx); // mbedtls_x509_crt_parse expects null-terminated string for PEM // Add null terminator if not present std::string pem_str(pem, len); int ret = mbedtls_x509_crt_parse( &mctx->ca_chain, reinterpret_cast(pem_str.c_str()), pem_str.size() + 1); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); return true; } bool load_ca_file(ctx_t ctx, const char *file_path) { if (!ctx || !file_path) { return false; } auto mctx = static_cast(ctx); int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, file_path); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); return true; } bool load_ca_dir(ctx_t ctx, const char *dir_path) { if (!ctx || !dir_path) { return false; } auto mctx = static_cast(ctx); int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, dir_path); if (ret < 0) { // Returns number of certs on success, negative on error impl::mbedtls_last_error() = ret; return false; } mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); return true; } bool load_system_certs(ctx_t ctx) { if (!ctx) { return false; } auto mctx = static_cast(ctx); bool loaded = false; #ifdef _WIN32 loaded = impl::enumerate_windows_system_certs( [&](const unsigned char *data, size_t len) { return mbedtls_x509_crt_parse_der(&mctx->ca_chain, data, len) == 0; }); #elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) loaded = impl::enumerate_macos_keychain_certs( [&](const unsigned char *data, size_t len) { return mbedtls_x509_crt_parse_der(&mctx->ca_chain, data, len) == 0; }); #else for (auto path = impl::system_ca_paths(); *path; ++path) { if (mbedtls_x509_crt_parse_file(&mctx->ca_chain, *path) >= 0) { loaded = true; break; } } if (!loaded) { for (auto dir = impl::system_ca_dirs(); *dir; ++dir) { if (mbedtls_x509_crt_parse_path(&mctx->ca_chain, *dir) >= 0) { loaded = true; break; } } } #endif if (loaded) { mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); } return loaded; } bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password) { if (!ctx || !cert || !key) { return false; } auto mctx = static_cast(ctx); // Parse certificate std::string cert_str(cert); int ret = mbedtls_x509_crt_parse( &mctx->own_cert, reinterpret_cast(cert_str.c_str()), cert_str.size() + 1); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Parse private key std::string key_str(key); const unsigned char *pwd = password ? reinterpret_cast(password) : nullptr; size_t pwd_len = password ? strlen(password) : 0; #ifdef CPPHTTPLIB_MBEDTLS_V3 ret = mbedtls_pk_parse_key( &mctx->own_key, reinterpret_cast(key_str.c_str()), key_str.size() + 1, pwd, pwd_len, mbedtls_ctr_drbg_random, &mctx->ctr_drbg); #else ret = mbedtls_pk_parse_key( &mctx->own_key, reinterpret_cast(key_str.c_str()), key_str.size() + 1, pwd, pwd_len); #endif if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Verify that the certificate and private key match #ifdef CPPHTTPLIB_MBEDTLS_V3 ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key, mbedtls_ctr_drbg_random, &mctx->ctr_drbg); #else ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key); #endif if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } return true; } bool set_client_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password) { if (!ctx || !cert_path || !key_path) { return false; } auto mctx = static_cast(ctx); // Parse certificate file int ret = mbedtls_x509_crt_parse_file(&mctx->own_cert, cert_path); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Parse private key file #ifdef CPPHTTPLIB_MBEDTLS_V3 ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password, mbedtls_ctr_drbg_random, &mctx->ctr_drbg); #else ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password); #endif if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Verify that the certificate and private key match #ifdef CPPHTTPLIB_MBEDTLS_V3 ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key, mbedtls_ctr_drbg_random, &mctx->ctr_drbg); #else ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key); #endif if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } return true; } void set_verify_client(ctx_t ctx, bool require) { if (!ctx) { return; } auto mctx = static_cast(ctx); mctx->verify_client = require; if (require) { mbedtls_ssl_conf_authmode(&mctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); } else { // If a verify callback is set, use OPTIONAL mode to ensure the callback // is called (matching OpenSSL behavior). Otherwise use NONE. mbedtls_ssl_conf_authmode(&mctx->conf, mctx->has_verify_callback ? MBEDTLS_SSL_VERIFY_OPTIONAL : MBEDTLS_SSL_VERIFY_NONE); } } session_t create_session(ctx_t ctx, socket_t sock) { if (!ctx || sock == INVALID_SOCKET) { return nullptr; } auto mctx = static_cast(ctx); auto session = new (std::nothrow) impl::MbedTlsSession(); if (!session) { return nullptr; } session->sock = sock; int ret = mbedtls_ssl_setup(&session->ssl, &mctx->conf); if (ret != 0) { impl::mbedtls_last_error() = ret; delete session; return nullptr; } // Explicitly opt out of in-handshake hostname verification by default; // since Mbed TLS 3.6.4 a client handshake with certificate verification // fails outright when no hostname was set. set_sni() installs the real // hostname for DNS hosts; for IP hosts (where SNI must not be set) the // caller verifies the certificate identity post-handshake via // verify_hostname(). mbedtls_ssl_set_hostname(&session->ssl, nullptr); // Set BIO callbacks mbedtls_ssl_set_bio(&session->ssl, &session->sock, impl::mbedtls_net_send_cb, impl::mbedtls_net_recv_cb, nullptr); // Set per-session verify callback with session pointer if callback is // registered if (mctx->has_verify_callback) { mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback, session); } return static_cast(session); } void free_session(session_t session) { if (session) { delete static_cast(session); } } bool set_sni(session_t session, const char *hostname) { if (!session || !hostname) { return false; } auto msession = static_cast(session); int ret = mbedtls_ssl_set_hostname(&msession->ssl, hostname); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } 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); } TlsError connect(session_t session) { TlsError err; if (!session) { err.code = ErrorCode::Fatal; return err; } auto msession = static_cast(session); int ret = mbedtls_ssl_handshake(&msession->ssl); if (ret == 0) { err.code = ErrorCode::Success; } else { err.code = impl::map_mbedtls_error(ret, err.sys_errno); err.backend_code = static_cast(-ret); impl::mbedtls_last_error() = ret; } return err; } TlsError accept(session_t session) { // Same as connect for Mbed TLS - handshake works for both client and server auto result = connect(session); // After successful handshake, capture SNI from thread-local storage if (result.code == ErrorCode::Success && session) { auto msession = static_cast(session); msession->sni_hostname = std::move(impl::mbedpending_sni()); impl::mbedpending_sni().clear(); } return result; } bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { if (!session) { if (err) { err->code = ErrorCode::Fatal; } return false; } auto msession = static_cast(session); // Set socket to non-blocking mode detail::set_nonblocking(sock, true); auto cleanup = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); int ret; while ((ret = mbedtls_ssl_handshake(&msession->ssl)) != 0) { if (ret == MBEDTLS_ERR_SSL_WANT_READ) { if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } } // TlsError or timeout if (err) { err->code = impl::map_mbedtls_error(ret, err->sys_errno); err->backend_code = static_cast(-ret); } impl::mbedtls_last_error() = ret; return false; } if (err) { err->code = ErrorCode::Success; } return true; } bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { // Same implementation as connect for Mbed TLS bool result = connect_nonblocking(session, sock, timeout_sec, timeout_usec, err); // After successful handshake, capture SNI from thread-local storage if (result && session) { auto msession = static_cast(session); msession->sni_hostname = std::move(impl::mbedpending_sni()); impl::mbedpending_sni().clear(); } return result; } ssize_t read(session_t session, void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto msession = static_cast(session); int ret = mbedtls_ssl_read(&msession->ssl, static_cast(buf), len); if (ret > 0) { err.code = ErrorCode::Success; return static_cast(ret); } if (ret == 0) { err.code = ErrorCode::PeerClosed; return 0; } err.code = impl::map_mbedtls_error(ret, err.sys_errno); err.backend_code = static_cast(-ret); impl::mbedtls_last_error() = ret; // mbedTLS signals a clean close_notify via a negative error code rather // than 0; surface it as a clean EOF the way OpenSSL/wolfSSL do. if (err.code == ErrorCode::PeerClosed) { return 0; } return -1; } ssize_t write(session_t session, const void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto msession = static_cast(session); int ret = mbedtls_ssl_write(&msession->ssl, static_cast(buf), len); if (ret > 0) { err.code = ErrorCode::Success; return static_cast(ret); } if (ret == 0) { err.code = ErrorCode::PeerClosed; return 0; } err.code = impl::map_mbedtls_error(ret, err.sys_errno); err.backend_code = static_cast(-ret); impl::mbedtls_last_error() = ret; return -1; } int pending(const_session_t session) { if (!session) { return 0; } auto msession = static_cast(const_cast(session)); return static_cast(mbedtls_ssl_get_bytes_avail(&msession->ssl)); } void shutdown(session_t session, bool graceful) { if (!session) { return; } auto msession = static_cast(session); if (graceful) { // Try to send close_notify, but don't block forever int ret; int attempts = 0; while ((ret = mbedtls_ssl_close_notify(&msession->ssl)) != 0 && attempts < 3) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { break; } attempts++; } } } bool is_peer_closed(session_t session, socket_t sock) { if (!session || sock == INVALID_SOCKET) { return true; } auto msession = static_cast(session); // Check if there's already decrypted data available in the TLS buffer // If so, the connection is definitely alive if (mbedtls_ssl_get_bytes_avail(&msession->ssl) > 0) { return false; } // Set socket to non-blocking to avoid blocking on read detail::set_nonblocking(sock, true); auto cleanup = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); // Try a 1-byte read to check connection status // Note: This will consume the byte if data is available, but for the // purpose of checking if peer is closed, this should be acceptable // since we're only called when we expect the connection might be closing unsigned char buf; int ret = mbedtls_ssl_read(&msession->ssl, &buf, 1); // If we got data or WANT_READ (would block), connection is alive if (ret > 0 || ret == MBEDTLS_ERR_SSL_WANT_READ) { return false; } // If we get a peer close notify or a connection reset, the peer is closed return ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || ret == MBEDTLS_ERR_NET_CONN_RESET || ret == 0; } cert_t get_peer_cert(const_session_t session) { if (!session) { return nullptr; } auto msession = static_cast(const_cast(session)); // Mbed TLS returns a pointer to the internal peer cert chain. // WARNING: This pointer is only valid while the session is active. // Do not use the certificate after calling free_session(). const mbedtls_x509_crt *cert = mbedtls_ssl_get_peer_cert(&msession->ssl); return const_cast(cert); } void free_cert(cert_t cert) { // Mbed TLS: peer certificate is owned by the SSL context. // No-op here, but callers should still call this for cross-backend // portability. (void)cert; } bool verify_hostname(cert_t cert, const char *hostname) { if (!cert || !hostname) { return false; } auto mcert = static_cast(cert); std::string host_str(hostname); // Check if hostname is an IP address (IPv4 or IPv6) unsigned char ip_bytes[16]; auto ip_len = impl::parse_ip_address(host_str, ip_bytes); auto is_ip = ip_len > 0; // Check Subject Alternative Names (SAN) // In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags // - DNS names: raw string bytes // - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6) const mbedtls_x509_sequence *san = &mcert->subject_alt_names; while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) { const unsigned char *p = san->buf.p; size_t len = san->buf.len; if (is_ip) { // For an IP host, only a matching iPAddress SAN of the same family // (4 bytes for IPv4, 16 bytes for IPv6) may authenticate it. if (len == ip_len && memcmp(p, ip_bytes, ip_len) == 0) { return true; } } else { // Check if this SAN is a DNS name (printable ASCII string) bool is_dns = len > 0; for (size_t i = 0; i < len && is_dns; i++) { if (p[i] < 32 || p[i] > 126) { is_dns = false; } } if (is_dns) { std::string san_name(reinterpret_cast(p), len); if (detail::match_hostname(san_name, host_str)) { return true; } } } san = san->next; } // Fallback: Check Common Name (CN) in subject. Skipped for IP-literal hosts: // an IP identity is only valid via an iPAddress SAN, never the CN (RFC 9110; // the OpenSSL backend's X509_check_ip behaves the same way). if (!is_ip) { char cn[256]; int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject); if (ret > 0) { std::string cn_str(cn); // Look for "CN=" in the DN string size_t cn_pos = cn_str.find("CN="); if (cn_pos != std::string::npos) { size_t start = cn_pos + 3; size_t end = cn_str.find(',', start); std::string cn_value = cn_str.substr(start, end == std::string::npos ? end : end - start); if (detail::match_hostname(cn_value, host_str)) { return true; } } } } return false; } uint64_t hostname_mismatch_code() { return static_cast(MBEDTLS_X509_BADCERT_CN_MISMATCH); } long get_verify_result(const_session_t session) { if (!session) { return -1; } auto msession = static_cast(const_cast(session)); uint32_t flags = mbedtls_ssl_get_verify_result(&msession->ssl); // Return 0 (X509_V_OK equivalent) if verification passed return flags == 0 ? 0 : static_cast(flags); } std::string get_cert_subject_cn(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); // Find the CN in the subject const mbedtls_x509_name *name = &x509->subject; while (name != nullptr) { if (MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &name->oid) == 0) { return std::string(reinterpret_cast(name->val.p), name->val.len); } name = name->next; } return ""; } std::string get_cert_issuer_name(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); // Build a human-readable issuer name string char buf[512]; int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &x509->issuer); if (ret < 0) return ""; return std::string(buf); } bool get_cert_sans(cert_t cert, std::vector &sans) { sans.clear(); if (!cert) return false; auto x509 = static_cast(cert); // Parse the Subject Alternative Name extension const mbedtls_x509_sequence *cur = &x509->subject_alt_names; while (cur != nullptr) { if (cur->buf.len > 0) { // Mbed TLS stores SAN as ASN.1 sequences // The tag byte indicates the type const unsigned char *p = cur->buf.p; size_t len = cur->buf.len; // First byte is the tag unsigned char tag = *p; p++; len--; // Parse length (simple single-byte length assumed) if (len > 0 && *p < 0x80) { size_t value_len = *p; p++; len--; if (value_len <= len) { SanEntry entry; // ASN.1 context tags for GeneralName switch (tag & 0x1F) { case 2: // dNSName entry.type = SanType::DNS; entry.value = std::string(reinterpret_cast(p), value_len); break; case 7: // iPAddress entry.type = SanType::IP; if (value_len == 4) { // IPv4 char buf[16]; snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]); entry.value = buf; } else if (value_len == 16) { // IPv6 char buf[64]; snprintf(buf, sizeof(buf), "%02x%02x:%02x%02x:%02x%02x:%02x%02x:" "%02x%02x:%02x%02x:%02x%02x:%02x%02x", p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[10], p[11], p[12], p[13], p[14], p[15]); entry.value = buf; } break; case 1: // rfc822Name (email) entry.type = SanType::EMAIL; entry.value = std::string(reinterpret_cast(p), value_len); break; case 6: // uniformResourceIdentifier entry.type = SanType::URI; entry.value = std::string(reinterpret_cast(p), value_len); break; default: entry.type = SanType::OTHER; break; } if (!entry.value.empty()) { sans.push_back(std::move(entry)); } } } } cur = cur->next; } return true; } bool get_cert_validity(cert_t cert, time_t ¬_before, time_t ¬_after) { if (!cert) return false; auto x509 = static_cast(cert); // Convert mbedtls_x509_time to time_t auto to_time_t = [](const mbedtls_x509_time &t) -> time_t { struct tm tm_time = {}; tm_time.tm_year = t.year - 1900; tm_time.tm_mon = t.mon - 1; tm_time.tm_mday = t.day; tm_time.tm_hour = t.hour; tm_time.tm_min = t.min; tm_time.tm_sec = t.sec; #ifdef _WIN32 return _mkgmtime(&tm_time); #else return timegm(&tm_time); #endif }; not_before = to_time_t(x509->valid_from); not_after = to_time_t(x509->valid_to); return true; } std::string get_cert_serial(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); // Convert serial number to hex string std::string result; result.reserve(x509->serial.len * 2); for (size_t i = 0; i < x509->serial.len; i++) { char hex[3]; snprintf(hex, sizeof(hex), "%02X", x509->serial.p[i]); result += hex; } return result; } bool get_cert_der(cert_t cert, std::vector &der) { if (!cert) return false; auto crt = static_cast(cert); if (!crt->raw.p || crt->raw.len == 0) return false; der.assign(crt->raw.p, crt->raw.p + crt->raw.len); return true; } const char *get_sni(const_session_t session) { if (!session) return nullptr; auto msession = static_cast(session); // For server: return SNI received from client during handshake if (!msession->sni_hostname.empty()) { return msession->sni_hostname.c_str(); } // For client: return the hostname set via set_sni if (!msession->hostname.empty()) { return msession->hostname.c_str(); } return nullptr; } uint64_t peek_error() { // Mbed TLS doesn't have an error queue, return the last error return static_cast(-impl::mbedtls_last_error()); } uint64_t get_error() { // Mbed TLS doesn't have an error queue, return and clear the last error uint64_t err = static_cast(-impl::mbedtls_last_error()); impl::mbedtls_last_error() = 0; return err; } std::string error_string(uint64_t code) { char buf[256]; mbedtls_strerror(-static_cast(code), buf, sizeof(buf)); return std::string(buf); } ca_store_t create_ca_store(const char *pem, size_t len) { auto *ca_chain = new (std::nothrow) mbedtls_x509_crt; if (!ca_chain) { return nullptr; } mbedtls_x509_crt_init(ca_chain); // mbedtls_x509_crt_parse expects null-terminated PEM int ret = mbedtls_x509_crt_parse(ca_chain, reinterpret_cast(pem), len + 1); // +1 for null terminator if (ret != 0) { // Try without +1 in case PEM is already null-terminated ret = mbedtls_x509_crt_parse( ca_chain, reinterpret_cast(pem), len); if (ret != 0) { mbedtls_x509_crt_free(ca_chain); delete ca_chain; return nullptr; } } return static_cast(ca_chain); } void free_ca_store(ca_store_t store) { if (store) { auto *ca_chain = static_cast(store); mbedtls_x509_crt_free(ca_chain); delete ca_chain; } } bool set_ca_store(ctx_t ctx, ca_store_t store) { if (!ctx || !store) { return false; } auto *mbed_ctx = static_cast(ctx); auto *ca_chain = static_cast(store); // Free existing CA chain mbedtls_x509_crt_free(&mbed_ctx->ca_chain); mbedtls_x509_crt_init(&mbed_ctx->ca_chain); // Copy the CA chain (deep copy) // Parse from the raw data of the source cert mbedtls_x509_crt *src = ca_chain; while (src != nullptr) { int ret = mbedtls_x509_crt_parse_der(&mbed_ctx->ca_chain, src->raw.p, src->raw.len); if (ret != 0) { free_ca_store(store); return false; } src = src->next; } // This function takes ownership of the store; the chain was deep-copied // above, so release the source free_ca_store(store); // Update the SSL config to use the new CA chain mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr); return true; } size_t get_ca_certs(ctx_t ctx, std::vector &certs) { certs.clear(); if (!ctx) { return 0; } auto *mbed_ctx = static_cast(ctx); // Iterate through the CA chain mbedtls_x509_crt *cert = &mbed_ctx->ca_chain; while (cert != nullptr && cert->raw.len > 0) { // Create a copy of the certificate for the caller auto *copy = new mbedtls_x509_crt; mbedtls_x509_crt_init(copy); int ret = mbedtls_x509_crt_parse_der(copy, cert->raw.p, cert->raw.len); if (ret == 0) { certs.push_back(static_cast(copy)); } else { mbedtls_x509_crt_free(copy); delete copy; } cert = cert->next; } return certs.size(); } std::vector get_ca_names(ctx_t ctx) { std::vector names; if (!ctx) { return names; } auto *mbed_ctx = static_cast(ctx); // Iterate through the CA chain mbedtls_x509_crt *cert = &mbed_ctx->ca_chain; while (cert != nullptr && cert->raw.len > 0) { char buf[512]; int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &cert->subject); if (ret > 0) { names.push_back(buf); } cert = cert->next; } return names; } bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem, const char *password) { if (!ctx || !cert_pem || !key_pem) { return false; } auto *mbed_ctx = static_cast(ctx); // Free existing certificate and key mbedtls_x509_crt_free(&mbed_ctx->own_cert); mbedtls_pk_free(&mbed_ctx->own_key); mbedtls_x509_crt_init(&mbed_ctx->own_cert); mbedtls_pk_init(&mbed_ctx->own_key); // Parse certificate PEM int ret = mbedtls_x509_crt_parse( &mbed_ctx->own_cert, reinterpret_cast(cert_pem), strlen(cert_pem) + 1); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Parse private key PEM #ifdef CPPHTTPLIB_MBEDTLS_V3 ret = mbedtls_pk_parse_key( &mbed_ctx->own_key, reinterpret_cast(key_pem), strlen(key_pem) + 1, password ? reinterpret_cast(password) : nullptr, password ? strlen(password) : 0, mbedtls_ctr_drbg_random, &mbed_ctx->ctr_drbg); #else ret = mbedtls_pk_parse_key( &mbed_ctx->own_key, reinterpret_cast(key_pem), strlen(key_pem) + 1, password ? reinterpret_cast(password) : nullptr, password ? strlen(password) : 0); #endif if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Configure SSL to use the new certificate and key ret = mbedtls_ssl_conf_own_cert(&mbed_ctx->conf, &mbed_ctx->own_cert, &mbed_ctx->own_key); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } return true; } bool update_server_client_ca(ctx_t ctx, const char *ca_pem) { if (!ctx || !ca_pem) { return false; } auto *mbed_ctx = static_cast(ctx); // Free existing CA chain mbedtls_x509_crt_free(&mbed_ctx->ca_chain); mbedtls_x509_crt_init(&mbed_ctx->ca_chain); // Parse CA PEM int ret = mbedtls_x509_crt_parse( &mbed_ctx->ca_chain, reinterpret_cast(ca_pem), strlen(ca_pem) + 1); if (ret != 0) { impl::mbedtls_last_error() = ret; return false; } // Update SSL config to use new CA chain mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr); return true; } bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (!ctx) { return false; } auto *mbed_ctx = static_cast(ctx); impl::get_verify_callback() = std::move(callback); mbed_ctx->has_verify_callback = static_cast(impl::get_verify_callback()); if (mbed_ctx->has_verify_callback) { // Set OPTIONAL mode to ensure callback is called even when verification // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set) mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); mbedtls_ssl_conf_verify(&mbed_ctx->conf, impl::mbedtls_verify_callback, nullptr); } else { mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr); } return true; } long get_verify_error(const_session_t session) { if (!session) { return -1; } auto *msession = static_cast(const_cast(session)); return static_cast(mbedtls_ssl_get_verify_result(&msession->ssl)); } std::string verify_error_string(long error_code) { if (error_code == 0) { return ""; } char buf[256]; mbedtls_x509_crt_verify_info(buf, sizeof(buf), "", static_cast(error_code)); // Remove trailing newline if present std::string result(buf); while (!result.empty() && (result.back() == '\n' || result.back() == ' ')) { result.pop_back(); } return result; } } // namespace tls #endif // CPPHTTPLIB_MBEDTLS_SUPPORT /* * Group 10: TLS abstraction layer - wolfSSL backend */ /* * wolfSSL Backend Implementation */ #ifdef CPPHTTPLIB_WOLFSSL_SUPPORT namespace tls { namespace impl { // wolfSSL session wrapper struct WolfSSLSession { WOLFSSL *ssl = nullptr; socket_t sock = INVALID_SOCKET; std::string hostname; // For client: set via set_sni std::string sni_hostname; // For server: received from client via SNI callback WolfSSLSession() = default; ~WolfSSLSession() { if (ssl) { wolfSSL_free(ssl); } } WolfSSLSession(const WolfSSLSession &) = delete; WolfSSLSession &operator=(const WolfSSLSession &) = delete; }; // Thread-local error code accessor for wolfSSL uint64_t &wolfssl_last_error() { static thread_local uint64_t err = 0; return err; } // Helper to map wolfSSL error to ErrorCode. // ssl_error is the value from wolfSSL_get_error(). // raw_ret is the raw return value from the wolfSSL call (for low-level error). ErrorCode map_wolfssl_error(WOLFSSL *ssl, int ssl_error, int &out_errno) { switch (ssl_error) { case SSL_ERROR_NONE: return ErrorCode::Success; case SSL_ERROR_WANT_READ: return ErrorCode::WantRead; case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite; case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed; case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError; default: if (ssl) { // wolfSSL stores the low-level error code as a negative value. // DOMAIN_NAME_MISMATCH (-322) indicates hostname verification failure. int low_err = ssl_error; // wolfSSL_get_error returns the low-level code if (low_err == DOMAIN_NAME_MISMATCH) { return ErrorCode::HostnameMismatch; } // Check verify result to distinguish cert verification from generic SSL // errors. long vr = wolfSSL_get_verify_result(ssl); if (vr != 0) { return ErrorCode::CertVerifyFailed; } } return ErrorCode::Fatal; } } // WolfSSLContext constructor/destructor implementations WolfSSLContext::WolfSSLContext() { wolfSSL_Init(); } WolfSSLContext::~WolfSSLContext() { if (ctx) { wolfSSL_CTX_free(ctx); } } // Thread-local storage for SNI captured during handshake std::string &wolfssl_pending_sni() { static thread_local std::string sni; return sni; } // SNI callback for wolfSSL server to capture client's SNI hostname int wolfssl_sni_callback(WOLFSSL *ssl, int *ret, void *exArg) { (void)ret; (void)exArg; void *name_data = nullptr; unsigned short name_len = wolfSSL_SNI_GetRequest(ssl, WOLFSSL_SNI_HOST_NAME, &name_data); if (name_data && name_len > 0) { wolfssl_pending_sni().assign(static_cast(name_data), name_len); } else { wolfssl_pending_sni().clear(); } return 0; // Continue regardless } // wolfSSL verify callback wrapper int wolfssl_verify_callback(int preverify_ok, WOLFSSL_X509_STORE_CTX *x509_ctx) { auto &callback = get_verify_callback(); if (!callback) { return preverify_ok; } WOLFSSL_X509 *cert = wolfSSL_X509_STORE_CTX_get_current_cert(x509_ctx); int depth = wolfSSL_X509_STORE_CTX_get_error_depth(x509_ctx); int err = wolfSSL_X509_STORE_CTX_get_error(x509_ctx); // Get the WOLFSSL object from the X509_STORE_CTX WOLFSSL *ssl = static_cast(wolfSSL_X509_STORE_CTX_get_ex_data( x509_ctx, wolfSSL_get_ex_data_X509_STORE_CTX_idx())); VerifyContext verify_ctx; verify_ctx.session = static_cast(ssl); verify_ctx.cert = static_cast(cert); verify_ctx.depth = depth; verify_ctx.preverify_ok = (preverify_ok != 0); verify_ctx.error_code = static_cast(err); if (err != 0) { verify_ctx.error_string = wolfSSL_X509_verify_cert_error_string(err); } else { verify_ctx.error_string = nullptr; } bool accepted = callback(verify_ctx); return accepted ? 1 : 0; } void set_wolfssl_password_cb(WOLFSSL_CTX *ctx, const char *password) { wolfSSL_CTX_set_default_passwd_cb_userdata(ctx, const_cast(password)); wolfSSL_CTX_set_default_passwd_cb( ctx, [](char *buf, int size, int /*rwflag*/, void *userdata) -> int { auto *pwd = static_cast(userdata); if (!pwd) return 0; auto len = static_cast(strlen(pwd)); if (len > size) len = size; memcpy(buf, pwd, static_cast(len)); return len; }); } } // namespace impl ctx_t create_client_context() { auto ctx = new (std::nothrow) impl::WolfSSLContext(); if (!ctx) { return nullptr; } ctx->is_server = false; WOLFSSL_METHOD *method = wolfTLSv1_2_client_method(); if (!method) { delete ctx; return nullptr; } ctx->ctx = wolfSSL_CTX_new(method); if (!ctx->ctx) { delete ctx; return nullptr; } // Default: verify peer certificate wolfSSL_CTX_set_verify(ctx->ctx, SSL_VERIFY_PEER, nullptr); return static_cast(ctx); } ctx_t create_server_context() { auto ctx = new (std::nothrow) impl::WolfSSLContext(); if (!ctx) { return nullptr; } ctx->is_server = true; WOLFSSL_METHOD *method = wolfTLSv1_2_server_method(); if (!method) { delete ctx; return nullptr; } ctx->ctx = wolfSSL_CTX_new(method); if (!ctx->ctx) { delete ctx; return nullptr; } // Default: don't verify client wolfSSL_CTX_set_verify(ctx->ctx, SSL_VERIFY_NONE, nullptr); // Enable SNI on server wolfSSL_CTX_SNI_SetOptions(ctx->ctx, WOLFSSL_SNI_HOST_NAME, WOLFSSL_SNI_CONTINUE_ON_MISMATCH); wolfSSL_CTX_set_servername_callback(ctx->ctx, impl::wolfssl_sni_callback); return static_cast(ctx); } void free_context(ctx_t ctx) { if (ctx) { delete static_cast(ctx); } } bool set_min_version(ctx_t ctx, Version version) { if (!ctx) { return false; } auto wctx = static_cast(ctx); int min_ver = WOLFSSL_TLSV1_2; if (version >= Version::TLS1_3) { min_ver = WOLFSSL_TLSV1_3; } return wolfSSL_CTX_SetMinVersion(wctx->ctx, min_ver) == WOLFSSL_SUCCESS; } bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) { if (!ctx || !pem) { return false; } auto wctx = static_cast(ctx); int ret = wolfSSL_CTX_load_verify_buffer( wctx->ctx, reinterpret_cast(pem), static_cast(len), SSL_FILETYPE_PEM); if (ret != SSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } wctx->ca_pem_data_.append(pem, len); return true; } bool load_ca_file(ctx_t ctx, const char *file_path) { if (!ctx || !file_path) { return false; } auto wctx = static_cast(ctx); int ret = wolfSSL_CTX_load_verify_locations(wctx->ctx, file_path, nullptr); if (ret != SSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } return true; } bool load_ca_dir(ctx_t ctx, const char *dir_path) { if (!ctx || !dir_path) { return false; } auto wctx = static_cast(ctx); int ret = wolfSSL_CTX_load_verify_locations(wctx->ctx, nullptr, dir_path); // wolfSSL may fail if the directory doesn't contain properly hashed certs. // Unlike OpenSSL which lazily loads certs from directories, wolfSSL scans // immediately. Return true even on failure since the CA file may have // already been loaded, matching OpenSSL's lenient behavior. (void)ret; return true; } bool load_system_certs(ctx_t ctx) { if (!ctx) { return false; } auto wctx = static_cast(ctx); bool loaded = false; #ifdef _WIN32 loaded = impl::enumerate_windows_system_certs( [&](const unsigned char *data, size_t len) { return wolfSSL_CTX_load_verify_buffer(wctx->ctx, data, static_cast(len), SSL_FILETYPE_ASN1) == SSL_SUCCESS; }); #elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) loaded = impl::enumerate_macos_keychain_certs( [&](const unsigned char *data, size_t len) { return wolfSSL_CTX_load_verify_buffer(wctx->ctx, data, static_cast(len), SSL_FILETYPE_ASN1) == SSL_SUCCESS; }); #else for (auto path = impl::system_ca_paths(); *path; ++path) { if (wolfSSL_CTX_load_verify_locations(wctx->ctx, *path, nullptr) == SSL_SUCCESS) { loaded = true; break; } } if (!loaded) { for (auto dir = impl::system_ca_dirs(); *dir; ++dir) { if (wolfSSL_CTX_load_verify_locations(wctx->ctx, nullptr, *dir) == SSL_SUCCESS) { loaded = true; break; } } } #endif return loaded; } bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, const char *password) { if (!ctx || !cert || !key) { return false; } auto wctx = static_cast(ctx); // Load certificate int ret = wolfSSL_CTX_use_certificate_buffer( wctx->ctx, reinterpret_cast(cert), static_cast(strlen(cert)), SSL_FILETYPE_PEM); if (ret != SSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } // Set password callback if password is provided if (password) { impl::set_wolfssl_password_cb(wctx->ctx, password); } // Load private key ret = wolfSSL_CTX_use_PrivateKey_buffer( wctx->ctx, reinterpret_cast(key), static_cast(strlen(key)), SSL_FILETYPE_PEM); if (ret != SSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } // Verify that the certificate and private key match return wolfSSL_CTX_check_private_key(wctx->ctx) == SSL_SUCCESS; } bool set_client_cert_file(ctx_t ctx, const char *cert_path, const char *key_path, const char *password) { if (!ctx || !cert_path || !key_path) { return false; } auto wctx = static_cast(ctx); // Load certificate file int ret = wolfSSL_CTX_use_certificate_file(wctx->ctx, cert_path, SSL_FILETYPE_PEM); if (ret != SSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } // Set password callback if password is provided if (password) { impl::set_wolfssl_password_cb(wctx->ctx, password); } // Load private key file ret = wolfSSL_CTX_use_PrivateKey_file(wctx->ctx, key_path, SSL_FILETYPE_PEM); if (ret != SSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } // Verify that the certificate and private key match return wolfSSL_CTX_check_private_key(wctx->ctx) == SSL_SUCCESS; } void set_verify_client(ctx_t ctx, bool require) { if (!ctx) { return; } auto wctx = static_cast(ctx); wctx->verify_client = require; if (require) { wolfSSL_CTX_set_verify( wctx->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, wctx->has_verify_callback ? impl::wolfssl_verify_callback : nullptr); } else { if (wctx->has_verify_callback) { wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, impl::wolfssl_verify_callback); } else { wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_NONE, nullptr); } } } session_t create_session(ctx_t ctx, socket_t sock) { if (!ctx || sock == INVALID_SOCKET) { return nullptr; } auto wctx = static_cast(ctx); auto session = new (std::nothrow) impl::WolfSSLSession(); if (!session) { return nullptr; } session->sock = sock; session->ssl = wolfSSL_new(wctx->ctx); if (!session->ssl) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); delete session; return nullptr; } wolfSSL_set_fd(session->ssl, static_cast(sock)); return static_cast(session); } void free_session(session_t session) { if (session) { delete static_cast(session); } } bool set_sni(session_t session, const char *hostname) { if (!session || !hostname) { return false; } auto wsession = static_cast(session); int ret = wolfSSL_UseSNI(wsession->ssl, WOLFSSL_SNI_HOST_NAME, hostname, static_cast(strlen(hostname))); if (ret != WOLFSSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } // Also set hostname for verification 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) { err.code = ErrorCode::Fatal; return err; } auto wsession = static_cast(session); int ret = wolfSSL_connect(wsession->ssl); if (ret == SSL_SUCCESS) { err.code = ErrorCode::Success; } else { int ssl_error = wolfSSL_get_error(wsession->ssl, ret); err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno); err.backend_code = static_cast(ssl_error); impl::wolfssl_last_error() = err.backend_code; } return err; } TlsError accept(session_t session) { TlsError err; if (!session) { err.code = ErrorCode::Fatal; return err; } auto wsession = static_cast(session); int ret = wolfSSL_accept(wsession->ssl); if (ret == SSL_SUCCESS) { err.code = ErrorCode::Success; // Capture SNI from thread-local storage after successful handshake wsession->sni_hostname = std::move(impl::wolfssl_pending_sni()); impl::wolfssl_pending_sni().clear(); } else { int ssl_error = wolfSSL_get_error(wsession->ssl, ret); err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno); err.backend_code = static_cast(ssl_error); impl::wolfssl_last_error() = err.backend_code; } return err; } bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { if (!session) { if (err) { err->code = ErrorCode::Fatal; } return false; } auto wsession = static_cast(session); // Set socket to non-blocking mode detail::set_nonblocking(sock, true); auto cleanup = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); int ret; while ((ret = wolfSSL_connect(wsession->ssl)) != SSL_SUCCESS) { int ssl_error = wolfSSL_get_error(wsession->ssl, ret); if (ssl_error == SSL_ERROR_WANT_READ) { if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } } else if (ssl_error == SSL_ERROR_WANT_WRITE) { if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } } // Error or timeout if (err) { err->code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err->sys_errno); err->backend_code = static_cast(ssl_error); } impl::wolfssl_last_error() = static_cast(ssl_error); return false; } if (err) { err->code = ErrorCode::Success; } return true; } bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec, time_t timeout_usec, TlsError *err) { if (!session) { if (err) { err->code = ErrorCode::Fatal; } return false; } auto wsession = static_cast(session); // Set socket to non-blocking mode detail::set_nonblocking(sock, true); auto cleanup = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); int ret; while ((ret = wolfSSL_accept(wsession->ssl)) != SSL_SUCCESS) { int ssl_error = wolfSSL_get_error(wsession->ssl, ret); if (ssl_error == SSL_ERROR_WANT_READ) { if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } } else if (ssl_error == SSL_ERROR_WANT_WRITE) { if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } } // Error or timeout if (err) { err->code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err->sys_errno); err->backend_code = static_cast(ssl_error); } impl::wolfssl_last_error() = static_cast(ssl_error); return false; } if (err) { err->code = ErrorCode::Success; } // Capture SNI from thread-local storage after successful handshake wsession->sni_hostname = std::move(impl::wolfssl_pending_sni()); impl::wolfssl_pending_sni().clear(); return true; } ssize_t read(session_t session, void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto wsession = static_cast(session); int ret = wolfSSL_read(wsession->ssl, buf, static_cast(len)); if (ret > 0) { err.code = ErrorCode::Success; return static_cast(ret); } if (ret == 0) { err.code = ErrorCode::PeerClosed; return 0; } int ssl_error = wolfSSL_get_error(wsession->ssl, ret); err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno); err.backend_code = static_cast(ssl_error); impl::wolfssl_last_error() = err.backend_code; return -1; } ssize_t write(session_t session, const void *buf, size_t len, TlsError &err) { if (!session || !buf) { err.code = ErrorCode::Fatal; return -1; } auto wsession = static_cast(session); int ret = wolfSSL_write(wsession->ssl, buf, static_cast(len)); if (ret > 0) { err.code = ErrorCode::Success; return static_cast(ret); } // wolfSSL_write returns 0 when the peer has sent a close_notify. // Treat this as an error (return -1) so callers don't spin in a // write loop adding zero to the offset. if (ret == 0) { err.code = ErrorCode::PeerClosed; return -1; } int ssl_error = wolfSSL_get_error(wsession->ssl, ret); err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno); err.backend_code = static_cast(ssl_error); impl::wolfssl_last_error() = err.backend_code; return -1; } int pending(const_session_t session) { if (!session) { return 0; } auto wsession = static_cast(const_cast(session)); return wolfSSL_pending(wsession->ssl); } void shutdown(session_t session, bool graceful) { if (!session) { return; } auto wsession = static_cast(session); if (graceful) { int ret; int attempts = 0; while ((ret = wolfSSL_shutdown(wsession->ssl)) != SSL_SUCCESS && attempts < 3) { int ssl_error = wolfSSL_get_error(wsession->ssl, ret); if (ssl_error != SSL_ERROR_WANT_READ && ssl_error != SSL_ERROR_WANT_WRITE) { break; } attempts++; } } else { wolfSSL_shutdown(wsession->ssl); } } bool is_peer_closed(session_t session, socket_t sock) { if (!session || sock == INVALID_SOCKET) { return true; } auto wsession = static_cast(session); // Check if there's already decrypted data available if (wolfSSL_pending(wsession->ssl) > 0) { return false; } // Set socket to non-blocking to avoid blocking on read detail::set_nonblocking(sock, true); auto cleanup = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); // Peek 1 byte to check connection status without consuming data unsigned char buf; int ret = wolfSSL_peek(wsession->ssl, &buf, 1); // If we got data or WANT_READ (would block), connection is alive if (ret > 0) { return false; } int ssl_error = wolfSSL_get_error(wsession->ssl, ret); if (ssl_error == SSL_ERROR_WANT_READ) { return false; } return ssl_error == SSL_ERROR_ZERO_RETURN || ssl_error == SSL_ERROR_SYSCALL || ret == 0; } cert_t get_peer_cert(const_session_t session) { if (!session) { return nullptr; } auto wsession = static_cast(const_cast(session)); WOLFSSL_X509 *cert = wolfSSL_get_peer_certificate(wsession->ssl); return static_cast(cert); } void free_cert(cert_t cert) { if (cert) { wolfSSL_X509_free(static_cast(cert)); } } bool verify_hostname(cert_t cert, const char *hostname) { if (!cert || !hostname) { return false; } auto x509 = static_cast(cert); std::string host_str(hostname); // Check if hostname is an IP address (IPv4 or IPv6) unsigned char ip_bytes[16]; auto ip_len = impl::parse_ip_address(host_str, ip_bytes); auto is_ip = ip_len > 0; // Check Subject Alternative Names auto *san_names = static_cast( wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr)); if (san_names) { int san_count = wolfSSL_sk_num(san_names); for (int i = 0; i < san_count; i++) { auto *names = static_cast(wolfSSL_sk_value(san_names, i)); if (!names) continue; if (!is_ip && names->type == WOLFSSL_GEN_DNS) { // DNS name unsigned char *dns_name = nullptr; int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, names->d.dNSName); if (dns_name && dns_len > 0) { std::string san_name(reinterpret_cast(dns_name), static_cast(dns_len)); XFREE(dns_name, nullptr, DYNAMIC_TYPE_OPENSSL); if (detail::match_hostname(san_name, host_str)) { wolfSSL_sk_free(san_names); return true; } } } else if (is_ip && names->type == WOLFSSL_GEN_IPADD) { // IP address: only an iPAddress SAN of the same family (4 bytes for // IPv4, 16 bytes for IPv6) may authenticate the host. unsigned char *ip_data = wolfSSL_ASN1_STRING_data(names->d.iPAddress); auto san_ip_len = wolfSSL_ASN1_STRING_length(names->d.iPAddress); if (ip_data && san_ip_len == static_cast(ip_len) && memcmp(ip_data, ip_bytes, ip_len) == 0) { wolfSSL_sk_free(san_names); return true; } } } wolfSSL_sk_free(san_names); } // Fallback: Check Common Name (CN) in subject. Skipped for IP-literal hosts: // an IP identity is only valid via an iPAddress SAN, never the CN (RFC 9110; // the OpenSSL backend's X509_check_ip behaves the same way). auto subject = is_ip ? nullptr : wolfSSL_X509_get_subject_name(x509); if (subject) { char cn[256] = {}; int cn_len = wolfSSL_X509_NAME_get_text_by_NID(subject, NID_commonName, cn, sizeof(cn)); if (cn_len > 0) { std::string cn_str(cn, static_cast(cn_len)); if (detail::match_hostname(cn_str, host_str)) { return true; } } } return false; } uint64_t hostname_mismatch_code() { return static_cast(DOMAIN_NAME_MISMATCH); } long get_verify_result(const_session_t session) { if (!session) { return -1; } auto wsession = static_cast(const_cast(session)); long result = wolfSSL_get_verify_result(wsession->ssl); return result; } std::string get_cert_subject_cn(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509); if (!subject) return ""; char cn[256] = {}; int cn_len = wolfSSL_X509_NAME_get_text_by_NID(subject, NID_commonName, cn, sizeof(cn)); if (cn_len <= 0) return ""; return std::string(cn, static_cast(cn_len)); } std::string get_cert_issuer_name(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); WOLFSSL_X509_NAME *issuer = wolfSSL_X509_get_issuer_name(x509); if (!issuer) return ""; char *name_str = wolfSSL_X509_NAME_oneline(issuer, nullptr, 0); if (!name_str) return ""; std::string result(name_str); XFREE(name_str, nullptr, DYNAMIC_TYPE_OPENSSL); return result; } bool get_cert_sans(cert_t cert, std::vector &sans) { sans.clear(); if (!cert) return false; auto x509 = static_cast(cert); auto *san_names = static_cast( wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr)); if (!san_names) return true; // No SANs is not an error int count = wolfSSL_sk_num(san_names); for (int i = 0; i < count; i++) { auto *name = static_cast(wolfSSL_sk_value(san_names, i)); if (!name) continue; SanEntry entry; switch (name->type) { case WOLFSSL_GEN_DNS: { entry.type = SanType::DNS; unsigned char *dns_name = nullptr; int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, name->d.dNSName); if (dns_name && dns_len > 0) { entry.value = std::string(reinterpret_cast(dns_name), static_cast(dns_len)); XFREE(dns_name, nullptr, DYNAMIC_TYPE_OPENSSL); } break; } case WOLFSSL_GEN_IPADD: { entry.type = SanType::IP; unsigned char *ip_data = wolfSSL_ASN1_STRING_data(name->d.iPAddress); int ip_len = wolfSSL_ASN1_STRING_length(name->d.iPAddress); if (ip_data && ip_len == 4) { char buf[16]; snprintf(buf, sizeof(buf), "%d.%d.%d.%d", ip_data[0], ip_data[1], ip_data[2], ip_data[3]); entry.value = buf; } else if (ip_data && ip_len == 16) { char buf[64]; snprintf(buf, sizeof(buf), "%02x%02x:%02x%02x:%02x%02x:%02x%02x:" "%02x%02x:%02x%02x:%02x%02x:%02x%02x", ip_data[0], ip_data[1], ip_data[2], ip_data[3], ip_data[4], ip_data[5], ip_data[6], ip_data[7], ip_data[8], ip_data[9], ip_data[10], ip_data[11], ip_data[12], ip_data[13], ip_data[14], ip_data[15]); entry.value = buf; } break; } case WOLFSSL_GEN_EMAIL: entry.type = SanType::EMAIL; { unsigned char *email = nullptr; int email_len = wolfSSL_ASN1_STRING_to_UTF8(&email, name->d.rfc822Name); if (email && email_len > 0) { entry.value = std::string(reinterpret_cast(email), static_cast(email_len)); XFREE(email, nullptr, DYNAMIC_TYPE_OPENSSL); } } break; case WOLFSSL_GEN_URI: entry.type = SanType::URI; { unsigned char *uri = nullptr; int uri_len = wolfSSL_ASN1_STRING_to_UTF8( &uri, name->d.uniformResourceIdentifier); if (uri && uri_len > 0) { entry.value = std::string(reinterpret_cast(uri), static_cast(uri_len)); XFREE(uri, nullptr, DYNAMIC_TYPE_OPENSSL); } } break; default: entry.type = SanType::OTHER; break; } if (!entry.value.empty()) { sans.push_back(std::move(entry)); } } wolfSSL_sk_free(san_names); return true; } bool get_cert_validity(cert_t cert, time_t ¬_before, time_t ¬_after) { if (!cert) return false; auto x509 = static_cast(cert); const WOLFSSL_ASN1_TIME *nb = wolfSSL_X509_get_notBefore(x509); const WOLFSSL_ASN1_TIME *na = wolfSSL_X509_get_notAfter(x509); if (!nb || !na) return false; // wolfSSL_ASN1_TIME_to_tm is available struct tm tm_nb = {}, tm_na = {}; if (wolfSSL_ASN1_TIME_to_tm(nb, &tm_nb) != WOLFSSL_SUCCESS) return false; if (wolfSSL_ASN1_TIME_to_tm(na, &tm_na) != WOLFSSL_SUCCESS) return false; #ifdef _WIN32 not_before = _mkgmtime(&tm_nb); not_after = _mkgmtime(&tm_na); #else not_before = timegm(&tm_nb); not_after = timegm(&tm_na); #endif return true; } std::string get_cert_serial(cert_t cert) { if (!cert) return ""; auto x509 = static_cast(cert); WOLFSSL_ASN1_INTEGER *serial_asn1 = wolfSSL_X509_get_serialNumber(x509); if (!serial_asn1) return ""; // Get the serial number data int len = serial_asn1->length; unsigned char *data = serial_asn1->data; if (!data || len <= 0) return ""; std::string result; result.reserve(static_cast(len) * 2); for (int i = 0; i < len; i++) { char hex[3]; snprintf(hex, sizeof(hex), "%02X", data[i]); result += hex; } return result; } bool get_cert_der(cert_t cert, std::vector &der) { if (!cert) return false; auto x509 = static_cast(cert); int der_len = 0; const unsigned char *der_data = wolfSSL_X509_get_der(x509, &der_len); if (!der_data || der_len <= 0) return false; der.assign(der_data, der_data + der_len); return true; } const char *get_sni(const_session_t session) { if (!session) return nullptr; auto wsession = static_cast(session); // For server: return SNI received from client during handshake if (!wsession->sni_hostname.empty()) { return wsession->sni_hostname.c_str(); } // For client: return the hostname set via set_sni if (!wsession->hostname.empty()) { return wsession->hostname.c_str(); } return nullptr; } uint64_t peek_error() { return static_cast(wolfSSL_ERR_peek_last_error()); } uint64_t get_error() { uint64_t err = impl::wolfssl_last_error(); impl::wolfssl_last_error() = 0; return err; } std::string error_string(uint64_t code) { char buf[256]; wolfSSL_ERR_error_string(static_cast(code), buf); return std::string(buf); } ca_store_t create_ca_store(const char *pem, size_t len) { if (!pem || len == 0) { return nullptr; } // Validate by attempting to load into a temporary ctx WOLFSSL_CTX *tmp_ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method()); if (!tmp_ctx) { return nullptr; } int ret = wolfSSL_CTX_load_verify_buffer( tmp_ctx, reinterpret_cast(pem), static_cast(len), SSL_FILETYPE_PEM); wolfSSL_CTX_free(tmp_ctx); if (ret != SSL_SUCCESS) { return nullptr; } return static_cast( new impl::WolfSSLCAStore{std::string(pem, len)}); } void free_ca_store(ca_store_t store) { delete static_cast(store); } bool set_ca_store(ctx_t ctx, ca_store_t store) { if (!ctx || !store) { return false; } auto *wctx = static_cast(ctx); auto *ca = static_cast(store); int ret = wolfSSL_CTX_load_verify_buffer( wctx->ctx, reinterpret_cast(ca->pem_data.data()), static_cast(ca->pem_data.size()), SSL_FILETYPE_PEM); if (ret == SSL_SUCCESS) { wctx->ca_pem_data_ += ca->pem_data; } // This function takes ownership of the store; the PEM data was copied into // the context, so release the source free_ca_store(store); return ret == SSL_SUCCESS; } size_t get_ca_certs(ctx_t ctx, std::vector &certs) { certs.clear(); if (!ctx) { return 0; } auto *wctx = static_cast(ctx); if (wctx->ca_pem_data_.empty()) { return 0; } const std::string &pem = wctx->ca_pem_data_; const std::string begin_marker = "-----BEGIN CERTIFICATE-----"; const std::string end_marker = "-----END CERTIFICATE-----"; size_t pos = 0; while ((pos = pem.find(begin_marker, pos)) != std::string::npos) { size_t end_pos = pem.find(end_marker, pos); if (end_pos == std::string::npos) { break; } end_pos += end_marker.size(); std::string cert_pem = pem.substr(pos, end_pos - pos); WOLFSSL_X509 *x509 = wolfSSL_X509_load_certificate_buffer( reinterpret_cast(cert_pem.data()), static_cast(cert_pem.size()), WOLFSSL_FILETYPE_PEM); if (x509) { certs.push_back(static_cast(x509)); } pos = end_pos; } return certs.size(); } std::vector get_ca_names(ctx_t ctx) { std::vector names; if (!ctx) { return names; } auto *wctx = static_cast(ctx); if (wctx->ca_pem_data_.empty()) { return names; } const std::string &pem = wctx->ca_pem_data_; const std::string begin_marker = "-----BEGIN CERTIFICATE-----"; const std::string end_marker = "-----END CERTIFICATE-----"; size_t pos = 0; while ((pos = pem.find(begin_marker, pos)) != std::string::npos) { size_t end_pos = pem.find(end_marker, pos); if (end_pos == std::string::npos) { break; } end_pos += end_marker.size(); std::string cert_pem = pem.substr(pos, end_pos - pos); WOLFSSL_X509 *x509 = wolfSSL_X509_load_certificate_buffer( reinterpret_cast(cert_pem.data()), static_cast(cert_pem.size()), WOLFSSL_FILETYPE_PEM); if (x509) { WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509); if (subject) { char *name_str = wolfSSL_X509_NAME_oneline(subject, nullptr, 0); if (name_str) { names.push_back(name_str); XFREE(name_str, nullptr, DYNAMIC_TYPE_OPENSSL); } } wolfSSL_X509_free(x509); } pos = end_pos; } return names; } bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem, const char *password) { if (!ctx || !cert_pem || !key_pem) { return false; } auto *wctx = static_cast(ctx); // Load new certificate int ret = wolfSSL_CTX_use_certificate_buffer( wctx->ctx, reinterpret_cast(cert_pem), static_cast(strlen(cert_pem)), SSL_FILETYPE_PEM); if (ret != SSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } // Set password if provided if (password) { impl::set_wolfssl_password_cb(wctx->ctx, password); } // Load new private key ret = wolfSSL_CTX_use_PrivateKey_buffer( wctx->ctx, reinterpret_cast(key_pem), static_cast(strlen(key_pem)), SSL_FILETYPE_PEM); if (ret != SSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } return true; } bool update_server_client_ca(ctx_t ctx, const char *ca_pem) { if (!ctx || !ca_pem) { return false; } auto *wctx = static_cast(ctx); int ret = wolfSSL_CTX_load_verify_buffer( wctx->ctx, reinterpret_cast(ca_pem), static_cast(strlen(ca_pem)), SSL_FILETYPE_PEM); if (ret != SSL_SUCCESS) { impl::wolfssl_last_error() = static_cast(wolfSSL_ERR_peek_last_error()); return false; } return true; } bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { if (!ctx) { return false; } auto *wctx = static_cast(ctx); impl::get_verify_callback() = std::move(callback); wctx->has_verify_callback = static_cast(impl::get_verify_callback()); if (wctx->has_verify_callback) { wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, impl::wolfssl_verify_callback); } else { wolfSSL_CTX_set_verify( wctx->ctx, wctx->verify_client ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) : SSL_VERIFY_NONE, nullptr); } return true; } long get_verify_error(const_session_t session) { if (!session) { return -1; } auto *wsession = static_cast(const_cast(session)); return wolfSSL_get_verify_result(wsession->ssl); } std::string verify_error_string(long error_code) { if (error_code == 0) { return ""; } const char *str = wolfSSL_X509_verify_cert_error_string(static_cast(error_code)); return str ? std::string(str) : std::string(); } } // namespace tls #endif // CPPHTTPLIB_WOLFSSL_SUPPORT // WebSocket implementation namespace ws { bool WebSocket::send_frame(Opcode op, const char *data, size_t len, bool fin) { std::lock_guard lock(write_mutex_); if (closed_) { return false; } return detail::write_websocket_frame(strm_, op, data, len, fin, !is_server_); } ReadResult WebSocket::read(std::string &msg) { while (!closed_) { Opcode opcode; std::string payload; bool fin; if (!impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_, CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) { closed_ = true; return Fail; } switch (opcode) { case Opcode::Ping: { std::lock_guard lock(write_mutex_); detail::write_websocket_frame(strm_, Opcode::Pong, payload.data(), payload.size(), true, !is_server_); continue; } case Opcode::Pong: { std::lock_guard lock(ping_mutex_); unacked_pings_ = 0; continue; } case Opcode::Close: { if (!closed_.exchange(true)) { // Echo close frame back std::lock_guard lock(write_mutex_); detail::write_websocket_frame(strm_, Opcode::Close, payload.data(), payload.size(), true, !is_server_); } return Fail; } case Opcode::Text: case Opcode::Binary: { auto result = opcode == Opcode::Text ? Text : Binary; msg = std::move(payload); // Handle fragmentation if (!fin) { while (true) { Opcode cont_opcode; std::string cont_payload; bool cont_fin; if (!impl::read_websocket_frame( strm_, cont_opcode, cont_payload, cont_fin, is_server_, CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) { closed_ = true; return Fail; } if (cont_opcode == Opcode::Ping) { std::lock_guard lock(write_mutex_); detail::write_websocket_frame( strm_, Opcode::Pong, cont_payload.data(), cont_payload.size(), true, !is_server_); continue; } if (cont_opcode == Opcode::Pong) { std::lock_guard lock(ping_mutex_); unacked_pings_ = 0; continue; } if (cont_opcode == Opcode::Close) { if (!closed_.exchange(true)) { std::lock_guard lock(write_mutex_); detail::write_websocket_frame( strm_, Opcode::Close, cont_payload.data(), cont_payload.size(), true, !is_server_); } return Fail; } // RFC 6455: continuation frames must use opcode 0x0 if (cont_opcode != Opcode::Continuation) { closed_ = true; return Fail; } msg += cont_payload; if (msg.size() > CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH) { closed_ = true; return Fail; } if (cont_fin) { break; } } } // RFC 6455 Section 5.6: text frames must contain valid UTF-8 if (result == Text && !impl::is_valid_utf8(msg)) { close(CloseStatus::InvalidPayload, "invalid UTF-8"); return Fail; } return result; } default: closed_ = true; return Fail; } } return Fail; } bool WebSocket::send(const std::string &data) { return send_frame(Opcode::Text, data.data(), data.size()); } bool WebSocket::send(const char *data, size_t len) { return send_frame(Opcode::Binary, data, len); } void WebSocket::close(CloseStatus status, const std::string &reason) { if (closed_.exchange(true)) { return; } ping_cv_.notify_all(); std::string payload; auto code = static_cast(status); payload.push_back(static_cast((code >> 8) & 0xFF)); payload.push_back(static_cast(code & 0xFF)); // RFC 6455 Section 5.5: control frame payload must not exceed 125 bytes // Close frame has 2-byte status code, so reason is limited to 123 bytes payload += reason.substr(0, 123); { std::lock_guard lock(write_mutex_); detail::write_websocket_frame(strm_, Opcode::Close, payload.data(), payload.size(), true, !is_server_); } // RFC 6455 Section 7.1.1: after sending a Close frame, wait for the peer's // Close response before closing the TCP connection. Use a short timeout to // avoid hanging if the peer doesn't respond. strm_.set_read_timeout(CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND, 0); Opcode op; std::string resp; bool fin; while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125)) { if (op == Opcode::Close) { break; } } } WebSocket::~WebSocket() { { std::lock_guard lock(ping_mutex_); closed_ = true; } ping_cv_.notify_all(); if (ping_thread_.joinable()) { ping_thread_.join(); } } void WebSocket::start_heartbeat() { if (ping_interval_sec_ == 0) { return; } ping_thread_ = std::thread([this]() { std::unique_lock lock(ping_mutex_); while (!closed_) { ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_)); if (closed_) { break; } // If the peer has failed to respond to the previous pings, give up. // RFC 6455 does not define a pong-timeout mechanism; this is an // opt-in liveness check controlled by max_missed_pongs_. if (max_missed_pongs_ > 0 && unacked_pings_ >= max_missed_pongs_) { lock.unlock(); close(CloseStatus::GoingAway, "pong timeout"); return; } lock.unlock(); if (!send_frame(Opcode::Ping, nullptr, 0)) { lock.lock(); closed_ = true; break; } lock.lock(); unacked_pings_++; } }); } const Request &WebSocket::request() const { return req_; } bool WebSocket::is_open() const { return !closed_; } // WebSocketClient implementation WebSocketClient::WebSocketClient( const std::string &scheme_host_port_path, const Headers &headers) : headers_(headers) { detail::UrlComponents uc; if (detail::parse_url(scheme_host_port_path, uc) && !uc.scheme.empty() && !uc.host.empty() && !uc.path.empty()) { auto &scheme = uc.scheme; #ifdef CPPHTTPLIB_SSL_ENABLED if (scheme != "ws" && scheme != "wss") { #else if (scheme != "ws") { #endif #ifndef CPPHTTPLIB_NO_EXCEPTIONS std::string msg = "'" + scheme + "' scheme is not supported."; throw std::invalid_argument(msg); #endif return; } auto is_ssl = scheme == "wss"; host_ = std::move(uc.host); port_ = is_ssl ? 443 : 80; if (!uc.port.empty() && !detail::parse_port(uc.port, port_)) { return; } path_ = std::move(uc.path); if (!uc.query.empty()) { path_ += uc.query; } #ifdef CPPHTTPLIB_SSL_ENABLED is_ssl_ = is_ssl; if (is_ssl_) { // The context lives as long as the client so that CA configuration // survives reconnects; sessions are created per connection. tls_ctx_ = tls::create_client_context(); if (!tls_ctx_) { return; } } #else if (is_ssl) { return; } #endif is_valid_ = true; } } WebSocketClient::~WebSocketClient() { shutdown_and_close(); #ifdef CPPHTTPLIB_SSL_ENABLED if (tls_ctx_) { tls::free_context(tls_ctx_); tls_ctx_ = nullptr; } #endif } bool WebSocketClient::is_valid() const { return is_valid_; } void WebSocketClient::shutdown_and_close() { #ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl_) { if (tls_session_) { tls::shutdown(tls_session_, true); tls::free_session(tls_session_); tls_session_ = nullptr; } } #endif if (ws_ && ws_->is_open()) { ws_->close(); } ws_.reset(); if (sock_ != INVALID_SOCKET) { detail::shutdown_socket(sock_); detail::close_socket(sock_); sock_ = INVALID_SOCKET; } } bool WebSocketClient::create_stream(std::unique_ptr &strm) { #ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl_) { 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); certs_loaded_ = true; } if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_, server_certificate_verification_, read_timeout_sec_, read_timeout_usec_)) { return false; } strm = std::unique_ptr(new detail::SSLSocketStream( sock_, tls_session_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_)); return true; } #endif strm = std::unique_ptr( new detail::SocketStream(sock_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_)); return true; } bool WebSocketClient::connect() { if (!is_valid_) { return false; } shutdown_and_close(); // Check is custom IP specified for host_ std::string ip; auto it = addr_map_.find(host_); if (it != addr_map_.end()) { ip = it->second; } Error error; sock_ = detail::create_client_socket( 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; } std::unique_ptr strm; if (!create_stream(strm)) { shutdown_and_close(); return false; } std::string selected_subprotocol; if (!detail::perform_websocket_handshake(*strm, host_, port_, path_, headers_, selected_subprotocol)) { shutdown_and_close(); return false; } subprotocol_ = std::move(selected_subprotocol); Request req; req.method = "GET"; req.path = path_; ws_ = std::unique_ptr(new WebSocket(std::move(strm), req, false, websocket_ping_interval_sec_, websocket_max_missed_pongs_)); return true; } ReadResult WebSocketClient::read(std::string &msg) { if (!ws_) { return Fail; } return ws_->read(msg); } bool WebSocketClient::send(const std::string &data) { if (!ws_) { return false; } return ws_->send(data); } bool WebSocketClient::send(const char *data, size_t len) { if (!ws_) { return false; } return ws_->send(data, len); } void WebSocketClient::close(CloseStatus status, const std::string &reason) { if (ws_) { ws_->close(status, reason); } } bool WebSocketClient::is_open() const { return ws_ && ws_->is_open(); } const std::string &WebSocketClient::subprotocol() const { return subprotocol_; } void WebSocketClient::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; } void WebSocketClient::set_write_timeout(time_t sec, time_t usec) { write_timeout_sec_ = sec; write_timeout_usec_ = usec; } void WebSocketClient::set_websocket_ping_interval(time_t sec) { websocket_ping_interval_sec_ = sec; } void WebSocketClient::set_websocket_max_missed_pongs(int count) { websocket_max_missed_pongs_ = count; } void WebSocketClient::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; } void WebSocketClient::set_address_family(int family) { address_family_ = family; } void WebSocketClient::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; } void WebSocketClient::set_socket_options(SocketOptions socket_options) { socket_options_ = std::move(socket_options); } void WebSocketClient::set_connection_timeout(time_t sec, time_t usec) { connection_timeout_sec_ = sec; connection_timeout_usec_ = usec; } void WebSocketClient::set_interface(const std::string &intf) { interface_ = intf; } void WebSocketClient::set_hostname_addr_map( std::map addr_map) { addr_map_ = std::move(addr_map); } #ifdef CPPHTTPLIB_SSL_ENABLED void WebSocketClient::set_ca_cert_path(const std::string &path) { ca_cert_file_path_ = path; } 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); } } 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; } } void WebSocketClient::enable_server_certificate_verification(bool enabled) { server_certificate_verification_ = enabled; } void WebSocketClient::enable_system_ca(bool enabled) { system_ca_mode_ = enabled ? SystemCAMode::Enabled : SystemCAMode::Disabled; } #endif // CPPHTTPLIB_SSL_ENABLED } // namespace ws } // namespace httplib