* 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<size_t>(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;
+}
+
+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
+ auto 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() != 200) {
+ connected_.store(false);
+ // For certain errors, don't reconnect
+ if (result.status() == 204 || // No Content - server wants us to stop
+ result.status() == 404 || // Not Found
+ result.status() == 401 || // Unauthorized
+ result.status() == 403) { // Forbidden
+ if (on_error_) { on_error_(Error::Connection); }
+ break;
+ }
+
+ if (on_error_) { on_error_(Error::Connection); }
+
+ 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<SanEntry> &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<unsigned char> &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<cert_t> &certs);
+std::vector<std::string> 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<const char *>(optval),
+#else
+ optval,
+#endif
+ optlen) == 0;
+}
+
+bool set_socket_opt(socket_t sock, int level, int optname, int optval) {
+ return set_socket_opt_impl(sock, level, optname, &optval, sizeof(optval));
+}
+
+bool set_socket_opt_time(socket_t sock, int level, int optname,
+ time_t sec, time_t usec) {
+#ifdef _WIN32
+ auto timeout = static_cast<uint32_t>(sec * 1000 + usec / 1000);
+#else
+ timeval timeout;
+ timeout.tv_sec = static_cast<long>(sec);
+ timeout.tv_usec = static_cast<decltype(timeout.tv_usec)>(usec);
+#endif
+ return set_socket_opt_impl(sock, level, optname, &timeout, sizeof(timeout));
+}
+
bool is_hex(char c, int &v) {
if (isdigit(c)) {
v = c - '0';
static const size_t read_buff_size_ = 1024l * 4;
};
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-class SSLSocketStream final : public Stream {
-public:
- SSLSocketStream(
- socket_t sock, SSL *ssl, 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<std::chrono::steady_clock> 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;
- 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;
-
-private:
- socket_t sock_;
- SSL *ssl_;
- 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<std::chrono::steady_clock> start_time_;
-};
-#endif
-
bool keep_alive(const std::atomic<socket_t> &svr_sock, socket_t sock,
time_t keep_alive_timeout_sec) {
using namespace std::chrono;
return true;
}
-bool read_content_with_length(Stream &strm, size_t len,
- DownloadProgress progress,
- ContentReceiverWithProgress out) {
+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<size_t>::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;
auto read_len = static_cast<size_t>(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) { return false; }
+ 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<size_t>(n), r, len)) { return false; }
+ if (!out(buf, static_cast<size_t>(n), r, len)) {
+ return ReadContentResult::Error;
+ }
r += static_cast<size_t>(n);
if (progress) {
- if (!progress(r, len)) { return false; }
+ if (!progress(r, len)) { return ReadContentResult::Error; }
}
}
- return true;
-}
-
-void skip_content_with_length(Stream &strm, size_t len) {
- char buf[CPPHTTPLIB_RECV_BUFSIZ];
- size_t r = 0;
- while (r < len) {
- auto read_len = static_cast<size_t>(len - r);
- auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ));
- if (n <= 0) { return; }
- r += static_cast<size_t>(n);
- }
+ return ReadContentResult::Success;
}
-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_without_length(Stream &strm, size_t payload_max_length,
ContentReceiverWithProgress out) {
if (is_invalid_value) {
ret = false;
- } else if (len > payload_max_length) {
- exceed_payload_max_length = true;
- skip_content_with_length(strm, len);
- ret = false;
} else if (len > 0) {
- ret = read_content_with_length(strm, len, std::move(progress), out);
+ 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;
+ }
}
}
return false;
}
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-std::string message_digest(const std::string &s, const EVP_MD *algo) {
- auto context = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
- 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<unsigned int>(hash[i]);
+#ifdef _WIN32
+class WSInit {
+public:
+ WSInit() {
+ WSADATA wsaData;
+ if (WSAStartup(0x0002, &wsaData) == 0) is_valid_ = true;
}
- return ss.str();
-}
-
-std::string MD5(const std::string &s) {
- return message_digest(s, EVP_md5());
-}
+ ~WSInit() {
+ if (is_valid_) WSACleanup();
+ }
-std::string SHA_256(const std::string &s) {
- return message_digest(s, EVP_sha256());
-}
+ bool is_valid_ = false;
+};
-std::string SHA_512(const std::string &s) {
- return message_digest(s, EVP_sha512());
-}
-
-std::pair<std::string, std::string> make_digest_authentication_header(
- const Request &req, const std::map<std::string, std::string> &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 is_ssl_peer_could_be_closed(SSL *ssl, socket_t sock) {
- detail::set_nonblocking(sock, true);
- auto se = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });
-
- char buf[1];
- return !SSL_peek(ssl, buf, 1) &&
- SSL_get_error(ssl, 0) == SSL_ERROR_ZERO_RETURN;
-}
-
-#ifdef _WIN32
-// NOTE: This code came up with the following stackoverflow post:
-// https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store
-bool load_system_certs_on_windows(X509_STORE *store) {
- auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT");
- if (!hStore) { return false; }
-
- auto result = false;
- PCCERT_CONTEXT pContext = NULL;
- while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
- nullptr) {
- auto encoded_cert =
- static_cast<const unsigned char *>(pContext->pbCertEncoded);
-
- auto x509 = d2i_X509(NULL, &encoded_cert, pContext->cbCertEncoded);
- if (x509) {
- X509_STORE_add_cert(store, x509);
- X509_free(x509);
- result = true;
- }
- }
-
- CertFreeCertificateContext(pContext);
- CertCloseStore(hStore, 0);
-
- return result;
-}
-#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC
-template <typename T>
-using CFObjectPtr =
- std::unique_ptr<typename std::remove_pointer<T>::type, void (*)(CFTypeRef)>;
-
-void cf_object_ptr_deleter(CFTypeRef obj) {
- if (obj) { CFRelease(obj); }
-}
-
-bool retrieve_certs_from_keychain(CFObjectPtr<CFArrayRef> &certs) {
- CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef};
- CFTypeRef values[] = {kSecClassCertificate, kSecMatchLimitAll,
- kCFBooleanTrue};
-
- CFObjectPtr<CFDictionaryRef> query(
- CFDictionaryCreate(nullptr, reinterpret_cast<const void **>(keys), values,
- sizeof(keys) / sizeof(keys[0]),
- &kCFTypeDictionaryKeyCallBacks,
- &kCFTypeDictionaryValueCallBacks),
- cf_object_ptr_deleter);
-
- if (!query) { return false; }
-
- CFTypeRef security_items = nullptr;
- if (SecItemCopyMatching(query.get(), &security_items) != errSecSuccess ||
- CFArrayGetTypeID() != CFGetTypeID(security_items)) {
- return false;
- }
-
- certs.reset(reinterpret_cast<CFArrayRef>(security_items));
- return true;
-}
-
-bool retrieve_root_certs_from_keychain(CFObjectPtr<CFArrayRef> &certs) {
- CFArrayRef root_security_items = nullptr;
- if (SecTrustCopyAnchorCertificates(&root_security_items) != errSecSuccess) {
- return false;
- }
-
- certs.reset(root_security_items);
- return true;
-}
-
-bool add_certs_to_x509_store(CFArrayRef certs, X509_STORE *store) {
- auto result = false;
- for (auto i = 0; i < CFArrayGetCount(certs); ++i) {
- const auto cert = reinterpret_cast<const __SecCertificate *>(
- CFArrayGetValueAtIndex(certs, i));
-
- if (SecCertificateGetTypeID() != CFGetTypeID(cert)) { continue; }
-
- CFDataRef cert_data = nullptr;
- if (SecItemExport(cert, kSecFormatX509Cert, 0, nullptr, &cert_data) !=
- errSecSuccess) {
- continue;
- }
-
- CFObjectPtr<CFDataRef> cert_data_ptr(cert_data, cf_object_ptr_deleter);
-
- auto encoded_cert = static_cast<const unsigned char *>(
- CFDataGetBytePtr(cert_data_ptr.get()));
-
- auto x509 =
- d2i_X509(NULL, &encoded_cert, CFDataGetLength(cert_data_ptr.get()));
-
- if (x509) {
- X509_STORE_add_cert(store, x509);
- X509_free(x509);
- result = true;
- }
- }
-
- return result;
-}
-
-bool load_system_certs_on_macos(X509_STORE *store) {
- auto result = false;
- CFObjectPtr<CFArrayRef> certs(nullptr, cf_object_ptr_deleter);
- if (retrieve_certs_from_keychain(certs) && certs) {
- result = add_certs_to_x509_store(certs.get(), store);
- }
-
- if (retrieve_root_certs_from_keychain(certs) && certs) {
- result = add_certs_to_x509_store(certs.get(), store) || result;
- }
-
- return result;
-}
-#endif // _WIN32
-#endif // CPPHTTPLIB_OPENSSL_SUPPORT
-
-#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
+static WSInit wsinit_;
+#endif
bool parse_www_authenticate(const Response &res,
std::map<std::string, std::string> &auth,
bool is_field_value(const std::string &s) { return is_field_content(s); }
} // namespace fields
-
} // namespace detail
-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";
+/*
+ * Group 2: detail namespace - SSL common utilities
+ */
- default:
- case StatusCode::InternalServerError_500: return "Internal Server Error";
+#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<std::chrono::steady_clock> 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;
+ 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;
+
+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<std::chrono::steady_clock> 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, decltype(&EVP_MD_CTX_free)>(
+ 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<unsigned int>(hash[i]);
}
+
+ return ss.str();
}
-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::string MD5(const std::string &s) {
+ return message_digest(s, EVP_md5());
}
-std::ostream &operator<<(std::ostream &os, const Error &obj) {
- os << to_string(obj);
- os << " (" << static_cast<std::underlying_type<Error>::type>(obj) << ')';
- return os;
+std::string SHA_256(const std::string &s) {
+ return message_digest(s, EVP_sha256());
}
-std::string hosted_at(const std::string &hostname) {
- std::vector<std::string> addrs;
- hosted_at(hostname, addrs);
- if (addrs.empty()) { return std::string(); }
- return addrs[0];
+std::string SHA_512(const std::string &s) {
+ return message_digest(s, EVP_sha512());
+}
+#elif defined(CPPHTTPLIB_MBEDTLS_SUPPORT)
+namespace {
+template <size_t N>
+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<unsigned int>(hash[i]);
+ }
+ return ss.str();
}
+} // namespace
-void hosted_at(const std::string &hostname,
- std::vector<std::string> &addrs) {
- struct addrinfo hints;
- struct addrinfo *result;
+std::string MD5(const std::string &s) {
+ unsigned char hash[16];
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+ mbedtls_md5(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),
+ hash);
+#else
+ mbedtls_md5_ret(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),
+ hash);
+#endif
+ return hash_to_hex(hash);
+}
- memset(&hints, 0, sizeof(struct addrinfo));
- hints.ai_family = AF_UNSPEC;
- hints.ai_socktype = SOCK_STREAM;
- hints.ai_protocol = 0;
+std::string SHA_256(const std::string &s) {
+ unsigned char hash[32];
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+ mbedtls_sha256(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),
+ hash, 0);
+#else
+ mbedtls_sha256_ret(reinterpret_cast<const unsigned char *>(s.c_str()),
+ s.size(), hash, 0);
+#endif
+ return hash_to_hex(hash);
+}
- if (detail::getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints,
- &result, 0)) {
-#if defined __linux__ && !defined __ANDROID__
- res_init();
+std::string SHA_512(const std::string &s) {
+ unsigned char hash[64];
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+ mbedtls_sha512(reinterpret_cast<const unsigned char *>(s.c_str()), s.size(),
+ hash, 0);
+#else
+ mbedtls_sha512_ret(reinterpret_cast<const unsigned char *>(s.c_str()),
+ s.size(), hash, 0);
+#endif
+ return hash_to_hex(hash);
+}
#endif
- return;
- }
- auto se = detail::scope_exit([&] { freeaddrinfo(result); });
- for (auto rp = result; rp; rp = rp->ai_next) {
- const auto &addr =
- *reinterpret_cast<struct sockaddr_storage *>(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));
- }
- }
+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;
}
-std::string encode_uri_component(const std::string &value) {
- std::ostringstream escaped;
- escaped.fill('0');
- escaped << std::hex;
+template <typename T>
+bool process_server_socket_ssl(
+ const std::atomic<socket_t> &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);
+ });
+}
- for (auto c : value) {
- if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
- c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
- c == ')') {
- escaped << c;
+template <typename T>
+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<std::chrono::steady_clock> 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<std::string, std::string> make_digest_authentication_header(
+ const Request &req, const std::map<std::string, std::string> &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 {
- escaped << std::uppercase;
- escaped << '%' << std::setw(2)
- << static_cast<int>(static_cast<unsigned char>(c));
- escaped << std::nouppercase;
+ qop.clear();
}
}
- return escaped.str();
-}
+ std::string algo = "MD5";
+ if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); }
-std::string encode_uri(const std::string &value) {
- std::ostringstream escaped;
- escaped.fill('0');
- escaped << std::hex;
+ std::string response;
+ {
+ auto H = algo == "SHA-256" ? detail::SHA_256
+ : algo == "SHA-512" ? detail::SHA_512
+ : detail::MD5;
- for (auto c : value) {
- if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
- c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
- c == ')' || c == ';' || c == '/' || c == '?' || c == ':' || c == '@' ||
- c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#') {
- escaped << c;
+ 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 {
- escaped << std::uppercase;
- escaped << '%' << std::setw(2)
- << static_cast<int>(static_cast<unsigned char>(c));
- escaped << std::nouppercase;
+ response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce +
+ ":" + qop + ":" + H(A2));
}
}
- return escaped.str();
+ 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);
}
-std::string decode_uri_component(const std::string &value) {
- std::string result;
+bool match_hostname(const std::string &pattern,
+ const std::string &hostname) {
+ // Exact match (case-insensitive)
+ if (detail::case_ignore::equal(hostname, pattern)) { return true; }
- 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<char>(val);
- i += 2;
- } else {
- result += value[i];
- }
- } else {
- result += value[i];
- }
+ // Split both pattern and hostname into components by '.'
+ std::vector<std::string> pattern_components;
+ if (!pattern.empty()) {
+ split(pattern.data(), pattern.data() + pattern.size(), '.',
+ [&](const char *b, const char *e) {
+ pattern_components.emplace_back(b, e);
+ });
}
- return result;
-}
+ std::vector<std::string> host_components;
+ if (!hostname.empty()) {
+ split(hostname.data(), hostname.data() + hostname.size(), '.',
+ [&](const char *b, const char *e) {
+ host_components.emplace_back(b, e);
+ });
+ }
-std::string decode_uri(const std::string &value) {
- std::string result;
+ // Component count must match
+ if (host_components.size() != pattern_components.size()) { return false; }
- 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<char>(val);
- i += 2;
- } else {
- result += value[i];
+ // 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<std::string::difference_type>(
+ prefix_length),
+ h.begin(), [](const char ca, const char cb) {
+ return detail::case_ignore::to_lower(ca) ==
+ detail::case_ignore::to_lower(cb);
+ });
+ }
}
- } else {
- result += value[i];
+ if (!partial_match) { return false; }
}
+ ++itr;
}
- return result;
+ return true;
}
-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<unsigned char>(component[i]);
-
- // Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~"
- if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
- result += static_cast<char>(c);
- }
- // Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" /
- // "," / ";" / "="
- else if (c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' ||
- c == ')' || c == '*' || c == '+' || c == ',' || c == ';' ||
- c == '=') {
- result += static_cast<char>(c);
- }
- // Colon is allowed in path segments except first segment
- else if (c == ':') {
- result += static_cast<char>(c);
- }
- // @ is allowed in path
- else if (c == '@') {
- result += static_cast<char>(c);
- } else {
- result += '%';
- char hex[3];
- snprintf(hex, sizeof(hex), "%02X", c);
- result.append(hex, 2);
- }
+#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<unsigned char> &der_cert, const std::string &hostname,
+ bool verify_hostname, unsigned long &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<DWORD>(der_cert.size()));
+
+ if (!cert_context) {
+ out_error = GetLastError();
+ return false;
}
- 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
- 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<char>(val);
- i += 2; // 'XX'
- } else {
- result += component[i];
- }
- }
- } else {
- result += component[i];
- }
- }
- return result;
-}
+ auto cert_guard =
+ scope_exit([&] { CertFreeCertificateContext(cert_context); });
-std::string encode_query_component(const std::string &component,
- bool space_as_plus) {
- std::string result;
- result.reserve(component.size() * 3);
+ // Setup chain parameters
+ CERT_CHAIN_PARA chain_para = {};
+ chain_para.cbSize = sizeof(chain_para);
- for (size_t i = 0; i < component.size(); i++) {
- auto c = static_cast<unsigned char>(component[i]);
+ // 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);
- // Unreserved characters per RFC 3986
- if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
- result += static_cast<char>(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<char>(c);
- }
- }
- // Query-safe sub-delimiters (excluding & and = which are query delimiters)
- else if (c == '!' || c == '$' || c == '\'' || c == '(' || c == ')' ||
- c == '*' || c == ',' || c == ';') {
- result += static_cast<char>(c);
- }
- // Colon and @ are allowed in query
- else if (c == ':' || c == '@') {
- result += static_cast<char>(c);
- }
- // Forward slash is allowed in query values
- else if (c == '/') {
- result += static_cast<char>(c);
- }
- // Question mark is allowed in query values (after first ?)
- else if (c == '?') {
- result += static_cast<char>(c);
- } else {
- result += '%';
- char hex[3];
- snprintf(hex, sizeof(hex), "%02X", c);
- result.append(hex, 2);
- }
+ if (!chain_result || !chain_context) {
+ out_error = GetLastError();
+ return false;
}
- return result;
-}
-std::string decode_query_component(const std::string &component,
- bool plus_as_space) {
- std::string result;
- result.reserve(component.size());
+ auto chain_guard =
+ scope_exit([&] { CertFreeCertificateChain(chain_context); });
- for (size_t i = 0; i < component.size(); i++) {
- if (component[i] == '%' && i + 2 < component.size()) {
- std::string hex = component.substr(i + 1, 2);
- char *end;
- unsigned long value = std::strtoul(hex.c_str(), &end, 16);
- if (end == hex.c_str() + 2) {
- result += static_cast<char>(value);
- i += 2;
- } else {
- result += component[i];
- }
- } else if (component[i] == '+' && plus_as_space) {
- result += ' '; // + becomes space in form-urlencoded
- } else {
- result += component[i];
- }
+ // Check if chain has errors
+ if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) {
+ out_error = chain_context->TrustStatus.dwErrorStatus;
+ return false;
}
- 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;
-}
+ // 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
-// Header utilities
-std::pair<std::string, std::string>
-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++;
+ std::wstring whost;
+ if (verify_hostname) {
+ whost = u8string_to_wstring(hostname.c_str());
+ extra_policy_para.pwszServerName = const_cast<wchar_t *>(whost.c_str());
}
- return std::make_pair("Range", std::move(field));
-}
-
-std::pair<std::string, std::string>
-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<std::string, std::string>
-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
-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);
-}
+ 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;
-size_t Request::get_header_value_count(const std::string &key) const {
- auto r = headers.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
-}
+ CERT_CHAIN_POLICY_STATUS policy_status = {};
+ policy_status.cbSize = sizeof(policy_status);
-void Request::set_header(const std::string &key,
- const std::string &val) {
- if (detail::fields::is_field_name(key) &&
- detail::fields::is_field_value(val)) {
- headers.emplace(key, val);
+ if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chain_context,
+ &policy_para, &policy_status)) {
+ out_error = GetLastError();
+ return false;
}
-}
-
-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 {
- auto rng = trailers.equal_range(key);
- auto it = rng.first;
- std::advance(it, static_cast<ssize_t>(id));
- if (it != rng.second) { return it->second; }
- return std::string();
-}
-size_t Request::get_trailer_value_count(const std::string &key) const {
- auto r = trailers.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
-}
+ if (policy_status.dwError != 0) {
+ out_error = policy_status.dwError;
+ return false;
+ }
-bool Request::has_param(const std::string &key) const {
- return params.find(key) != params.end();
+ return true;
}
+#endif // _WIN32
-std::string Request::get_param_value(const std::string &key,
- size_t id) const {
- auto rng = params.equal_range(key);
- auto it = rng.first;
- std::advance(it, static_cast<ssize_t>(id));
- if (it != rng.second) { return it->second; }
- return std::string();
-}
+} // namespace detail
+#endif // CPPHTTPLIB_SSL_ENABLED
-size_t Request::get_param_value_count(const std::string &key) const {
- auto r = params.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
-}
+/*
+ * Group 3: httplib namespace - Non-SSL public API implementations
+ */
-bool Request::is_multipart_form_data() const {
- const auto &content_type = get_header_value("Content-Type");
- return !content_type.rfind("multipart/form-data", 0);
+void default_socket_options(socket_t sock) {
+ detail::set_socket_opt(sock, SOL_SOCKET,
+#ifdef SO_REUSEPORT
+ SO_REUSEPORT,
+#else
+ SO_REUSEADDR,
+#endif
+ 1);
}
-// 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<ssize_t>(id));
- if (it != rng.second) { return it->second.content; }
- return std::string();
+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 "";
}
-std::vector<std::string>
-MultipartFormData::get_fields(const std::string &key) const {
- std::vector<std::string> values;
- auto rng = fields.equal_range(key);
- for (auto it = rng.first; it != rng.second; it++) {
- values.push_back(it->second.content);
+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";
}
- return values;
}
-bool MultipartFormData::has_field(const std::string &key) const {
- return fields.find(key) != fields.end();
-}
+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;
+ }
-size_t MultipartFormData::get_field_count(const std::string &key) const {
- auto r = fields.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
+ return "Invalid";
}
-FormData MultipartFormData::get_file(const std::string &key,
- size_t id) const {
- auto rng = files.equal_range(key);
- auto it = rng.first;
- std::advance(it, static_cast<ssize_t>(id));
- if (it != rng.second) { return it->second; }
- return FormData();
+std::ostream &operator<<(std::ostream &os, const Error &obj) {
+ os << to_string(obj);
+ os << " (" << static_cast<std::underlying_type<Error>::type>(obj) << ')';
+ return os;
}
-std::vector<FormData>
-MultipartFormData::get_files(const std::string &key) const {
- std::vector<FormData> values;
- auto rng = files.equal_range(key);
- for (auto it = rng.first; it != rng.second; it++) {
- values.push_back(it->second);
- }
- return values;
+std::string hosted_at(const std::string &hostname) {
+ std::vector<std::string> addrs;
+ hosted_at(hostname, addrs);
+ if (addrs.empty()) { return std::string(); }
+ return addrs[0];
}
-bool MultipartFormData::has_file(const std::string &key) const {
- return files.find(key) != files.end();
-}
+void hosted_at(const std::string &hostname,
+ std::vector<std::string> &addrs) {
+ struct addrinfo hints;
+ struct addrinfo *result;
-size_t MultipartFormData::get_file_count(const std::string &key) const {
- auto r = files.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
-}
+ memset(&hints, 0, sizeof(struct addrinfo));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = 0;
-// Response implementation
-bool Response::has_header(const std::string &key) const {
- return headers.find(key) != headers.end();
-}
+ 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); });
-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);
+ for (auto rp = result; rp; rp = rp->ai_next) {
+ const auto &addr =
+ *reinterpret_cast<struct sockaddr_storage *>(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));
+ }
+ }
}
-size_t Response::get_header_value_count(const std::string &key) const {
- auto r = headers.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
-}
+std::string encode_uri_component(const std::string &value) {
+ std::ostringstream escaped;
+ escaped.fill('0');
+ escaped << std::hex;
-void Response::set_header(const std::string &key,
- const std::string &val) {
- if (detail::fields::is_field_name(key) &&
- detail::fields::is_field_value(val)) {
- headers.emplace(key, val);
+ for (auto c : value) {
+ if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
+ c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
+ c == ')') {
+ escaped << c;
+ } else {
+ escaped << std::uppercase;
+ escaped << '%' << std::setw(2)
+ << static_cast<int>(static_cast<unsigned char>(c));
+ escaped << std::nouppercase;
+ }
}
-}
-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 {
- auto rng = trailers.equal_range(key);
- auto it = rng.first;
- std::advance(it, static_cast<ssize_t>(id));
- if (it != rng.second) { return it->second; }
- return std::string();
+ return escaped.str();
}
-size_t Response::get_trailer_value_count(const std::string &key) const {
- auto r = trailers.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
-}
+std::string encode_uri(const std::string &value) {
+ std::ostringstream escaped;
+ escaped.fill('0');
+ escaped << std::hex;
-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;
+ for (auto c : value) {
+ if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
+ c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
+ c == ')' || c == ';' || c == '/' || c == '?' || c == ':' || c == '@' ||
+ c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#') {
+ escaped << c;
} else {
- this->status = StatusCode::Found_302;
+ escaped << std::uppercase;
+ escaped << '%' << std::setw(2)
+ << static_cast<int>(static_cast<unsigned char>(c));
+ escaped << std::nouppercase;
}
}
+
+ return escaped.str();
}
-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);
+std::string decode_uri_component(const std::string &value) {
+ std::string result;
- auto rng = headers.equal_range("Content-Type");
- headers.erase(rng.first, rng.second);
- set_header("Content-Type", content_type);
-}
+ 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<char>(val);
+ i += 2;
+ } else {
+ result += value[i];
+ }
+ } else {
+ result += value[i];
+ }
+ }
-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;
+ return result;
}
-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;
-}
+std::string decode_uri(const std::string &value) {
+ std::string result;
-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;
-}
+ 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<char>(val);
+ i += 2;
+ } else {
+ result += value[i];
+ }
+ } else {
+ result += value[i];
+ }
+ }
-void Response::set_file_content(const std::string &path,
- const std::string &content_type) {
- file_content_path_ = path;
- file_content_content_type_ = content_type;
+ return result;
}
-void Response::set_file_content(const std::string &path) {
- file_content_path_ = path;
-}
+std::string encode_path_component(const std::string &component) {
+ std::string result;
+ result.reserve(component.size() * 3);
-// Result implementation
-bool Result::has_request_header(const std::string &key) const {
- return request_headers_.find(key) != request_headers_.end();
-}
+ for (size_t i = 0; i < component.size(); i++) {
+ auto c = static_cast<unsigned char>(component[i]);
-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);
+ // Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~"
+ if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
+ result += static_cast<char>(c);
+ }
+ // Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" /
+ // "," / ";" / "="
+ else if (c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' ||
+ c == ')' || c == '*' || c == '+' || c == ',' || c == ';' ||
+ c == '=') {
+ result += static_cast<char>(c);
+ }
+ // Colon is allowed in path segments except first segment
+ else if (c == ':') {
+ result += static_cast<char>(c);
+ }
+ // @ is allowed in path
+ else if (c == '@') {
+ result += static_cast<char>(c);
+ } else {
+ result += '%';
+ char hex[3];
+ snprintf(hex, sizeof(hex), "%02X", c);
+ result.append(hex, 2);
+ }
+ }
+ return result;
}
-size_t
-Result::get_request_header_value_count(const std::string &key) const {
- auto r = request_headers_.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
-}
+std::string decode_path_component(const std::string &component) {
+ std::string result;
+ result.reserve(component.size());
-// Stream implementation
-ssize_t Stream::write(const char *ptr) {
- return write(ptr, strlen(ptr));
+ 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
+ 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<char>(val);
+ i += 2; // 'XX'
+ } else {
+ result += component[i];
+ }
+ }
+ } else {
+ result += component[i];
+ }
+ }
+ return result;
}
-ssize_t Stream::write(const std::string &s) {
- return write(s.data(), s.size());
-}
+std::string encode_query_component(const std::string &component,
+ bool space_as_plus) {
+ std::string result;
+ result.reserve(component.size() * 3);
-// BodyReader implementation
-ssize_t detail::BodyReader::read(char *buf, size_t len) {
- if (!stream) {
- last_error = Error::Connection;
- return -1;
- }
- if (eof) { return 0; }
+ for (size_t i = 0; i < component.size(); i++) {
+ auto c = static_cast<unsigned char>(component[i]);
- if (!chunked) {
- // Content-Length based reading
- if (bytes_read >= content_length) {
- eof = true;
- return 0;
+ // Unreserved characters per RFC 3986
+ if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
+ result += static_cast<char>(c);
}
-
- auto remaining = content_length - bytes_read;
- auto 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;
+ // Space handling
+ else if (c == ' ') {
+ if (space_as_plus) {
+ result += '+';
+ } else {
+ result += "%20";
+ }
}
- 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;
+ // Plus sign handling
+ else if (c == '+') {
+ if (space_as_plus) {
+ result += "%2B";
+ } else {
+ result += static_cast<char>(c);
+ }
+ }
+ // Query-safe sub-delimiters (excluding & and = which are query delimiters)
+ else if (c == '!' || c == '$' || c == '\'' || c == '(' || c == ')' ||
+ c == '*' || c == ',' || c == ';') {
+ result += static_cast<char>(c);
+ }
+ // Colon and @ are allowed in query
+ else if (c == ':' || c == '@') {
+ result += static_cast<char>(c);
+ }
+ // Forward slash is allowed in query values
+ else if (c == '/') {
+ result += static_cast<char>(c);
+ }
+ // Question mark is allowed in query values (after first ?)
+ else if (c == '?') {
+ result += static_cast<char>(c);
+ } else {
+ result += '%';
+ char hex[3];
+ snprintf(hex, sizeof(hex), "%02X", c);
+ result.append(hex, 2);
}
-
- bytes_read += static_cast<size_t>(n);
- if (bytes_read >= content_length) { eof = true; }
- return n;
}
+ return result;
+}
- // Chunked transfer encoding: delegate to shared decoder instance.
- if (!chunked_decoder) { chunked_decoder.reset(new ChunkedDecoder(*stream)); }
+std::string decode_query_component(const std::string &component,
+ bool plus_as_space) {
+ std::string result;
+ result.reserve(component.size());
- 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;
+ for (size_t i = 0; i < component.size(); i++) {
+ if (component[i] == '%' && i + 2 < component.size()) {
+ std::string hex = component.substr(i + 1, 2);
+ char *end;
+ unsigned long value = std::strtoul(hex.c_str(), &end, 16);
+ if (end == hex.c_str() + 2) {
+ result += static_cast<char>(value);
+ i += 2;
+ } else {
+ result += component[i];
+ }
+ } else if (component[i] == '+' && plus_as_space) {
+ result += ' '; // + becomes space in form-urlencoded
+ } else {
+ result += component[i];
+ }
}
-
- bytes_read += static_cast<size_t>(n);
- return n;
+ return result;
}
-namespace detail {
+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;
+}
-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);
+// Header utilities
+std::pair<std::string, std::string>
+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));
+}
- auto actual_timeout_msec =
- (std::min)(max_timeout_msec - duration_msec, timeout_msec);
+std::pair<std::string, std::string>
+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));
+}
- if (actual_timeout_msec < 0) { actual_timeout_msec = 0; }
+std::pair<std::string, std::string>
+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));
+}
- actual_timeout_sec = actual_timeout_msec / 1000;
- actual_timeout_usec = (actual_timeout_msec % 1000) * 1000;
+// 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);
}
-// 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<std::chrono::steady_clock> 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) {}
+bool Request::has_header(const std::string &key) const {
+ return detail::has_header(headers, key);
+}
-SocketStream::~SocketStream() = default;
+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);
+}
-bool SocketStream::is_readable() const {
- return read_buff_off_ < read_buff_content_size_;
+size_t Request::get_header_value_count(const std::string &key) const {
+ auto r = headers.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
}
-bool SocketStream::wait_readable() const {
- if (max_timeout_msec_ <= 0) {
- return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
+void Request::set_header(const std::string &key,
+ const std::string &val) {
+ if (detail::fields::is_field_name(key) &&
+ detail::fields::is_field_value(val)) {
+ headers.emplace(key, val);
}
-
- 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 &&
- is_socket_alive(sock_);
+bool Request::has_trailer(const std::string &key) const {
+ return trailers.find(key) != trailers.end();
}
-ssize_t SocketStream::read(char *ptr, size_t size) {
-#ifdef _WIN32
- size =
- (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
-#else
- size = (std::min)(size,
- static_cast<size_t>((std::numeric_limits<ssize_t>::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<ssize_t>(size);
- } else {
- memcpy(ptr, read_buff_.data() + read_buff_off_, remaining_size);
- read_buff_off_ += remaining_size;
- return static_cast<ssize_t>(remaining_size);
- }
- }
+std::string Request::get_trailer_value(const std::string &key,
+ size_t id) const {
+ auto rng = trailers.equal_range(key);
+ auto it = rng.first;
+ std::advance(it, static_cast<ssize_t>(id));
+ if (it != rng.second) { return it->second; }
+ return std::string();
+}
- if (!wait_readable()) {
- error_ = Error::Timeout;
- return -1;
- }
+size_t Request::get_trailer_value_count(const std::string &key) const {
+ auto r = trailers.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
+}
- read_buff_off_ = 0;
- read_buff_content_size_ = 0;
+bool Request::has_param(const std::string &key) const {
+ return params.find(key) != params.end();
+}
- 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<ssize_t>(size)) {
- memcpy(ptr, read_buff_.data(), static_cast<size_t>(n));
- return n;
- } else {
- memcpy(ptr, read_buff_.data(), size);
- read_buff_off_ = size;
- read_buff_content_size_ = static_cast<size_t>(n);
- return static_cast<ssize_t>(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;
- }
+std::string Request::get_param_value(const std::string &key,
+ size_t id) const {
+ auto rng = params.equal_range(key);
+ auto it = rng.first;
+ std::advance(it, static_cast<ssize_t>(id));
+ if (it != rng.second) { return it->second; }
+ return std::string();
}
-ssize_t SocketStream::write(const char *ptr, size_t size) {
- if (!wait_writable()) { return -1; }
+size_t Request::get_param_value_count(const std::string &key) const {
+ auto r = params.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
+}
-#if defined(_WIN32) && !defined(_WIN64)
- size =
- (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
-#endif
+bool Request::is_multipart_form_data() const {
+ const auto &content_type = get_header_value("Content-Type");
+ return !content_type.rfind("multipart/form-data", 0);
+}
- return send_socket(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS);
+// 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<ssize_t>(id));
+ if (it != rng.second) { return it->second.content; }
+ return std::string();
}
-void SocketStream::get_remote_ip_and_port(std::string &ip,
- int &port) const {
- return detail::get_remote_ip_and_port(sock_, ip, port);
+std::vector<std::string>
+MultipartFormData::get_fields(const std::string &key) const {
+ std::vector<std::string> values;
+ auto rng = fields.equal_range(key);
+ for (auto it = rng.first; it != rng.second; it++) {
+ values.push_back(it->second.content);
+ }
+ return values;
}
-void SocketStream::get_local_ip_and_port(std::string &ip,
- int &port) const {
- return detail::get_local_ip_and_port(sock_, ip, port);
+bool MultipartFormData::has_field(const std::string &key) const {
+ return fields.find(key) != fields.end();
}
-socket_t SocketStream::socket() const { return sock_; }
+size_t MultipartFormData::get_field_count(const std::string &key) const {
+ auto r = fields.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
+}
-time_t SocketStream::duration() const {
- return std::chrono::duration_cast<std::chrono::milliseconds>(
- std::chrono::steady_clock::now() - start_time_)
- .count();
+FormData MultipartFormData::get_file(const std::string &key,
+ size_t id) const {
+ auto rng = files.equal_range(key);
+ auto it = rng.first;
+ std::advance(it, static_cast<ssize_t>(id));
+ if (it != rng.second) { return it->second; }
+ return FormData();
}
-// 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<size_t>(len_read);
- return static_cast<ssize_t>(len_read);
+std::vector<FormData>
+MultipartFormData::get_files(const std::string &key) const {
+ std::vector<FormData> values;
+ auto rng = files.equal_range(key);
+ for (auto it = rng.first; it != rng.second; it++) {
+ values.push_back(it->second);
+ }
+ return values;
}
-ssize_t BufferStream::write(const char *ptr, size_t size) {
- buffer.append(ptr, size);
- return static_cast<ssize_t>(size);
+bool MultipartFormData::has_file(const std::string &key) const {
+ return files.find(key) != files.end();
}
-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[] = "/:";
+size_t MultipartFormData::get_file_count(const std::string &key) const {
+ auto r = files.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
+}
- // One past the last ending position of a path param substring
- std::size_t last_param_end = 0;
+// 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);
+}
-#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<std::string> param_name_set;
-#endif
+bool Response::has_header(const std::string &key) const {
+ return headers.find(key) != headers.end();
+}
- 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; }
+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);
+}
- static_fragments_.push_back(
- pattern.substr(last_param_end, marker_pos - last_param_end + 1));
+size_t Response::get_header_value_count(const std::string &key) const {
+ auto r = headers.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
+}
- const auto param_name_start = marker_pos + str_len(marker);
+void Response::set_header(const std::string &key,
+ const std::string &val) {
+ if (detail::fields::is_field_name(key) &&
+ detail::fields::is_field_value(val)) {
+ headers.emplace(key, val);
+ }
+}
+bool Response::has_trailer(const std::string &key) const {
+ return trailers.find(key) != trailers.end();
+}
- auto sep_pos = pattern.find(separator, param_name_start);
- if (sep_pos == std::string::npos) { sep_pos = pattern.length(); }
+std::string Response::get_trailer_value(const std::string &key,
+ size_t id) const {
+ auto rng = trailers.equal_range(key);
+ auto it = rng.first;
+ std::advance(it, static_cast<ssize_t>(id));
+ if (it != rng.second) { return it->second; }
+ return std::string();
+}
- auto param_name =
- pattern.substr(param_name_start, sep_pos - param_name_start);
+size_t Response::get_trailer_value_count(const std::string &key) const {
+ auto r = trailers.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
+}
-#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);
+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;
}
-#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());
+void Response::set_content(const char *s, size_t n,
+ const std::string &content_type) {
+ body.assign(s, n);
- // 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];
+ auto rng = headers.equal_range("Content-Type");
+ headers.erase(rng.first, rng.second);
+ set_header("Content-Type", content_type);
+}
- if (starting_pos + fragment.length() > request.path.length()) {
- return false;
- }
+void Response::set_content(const std::string &s,
+ const std::string &content_type) {
+ set_content(s.data(), s.size(), content_type);
+}
- // 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;
- }
+void Response::set_content(std::string &&s,
+ const std::string &content_type) {
+ body = std::move(s);
- starting_pos += fragment.length();
+ auto rng = headers.equal_range("Content-Type");
+ headers.erase(rng.first, rng.second);
+ set_header("Content-Type", content_type);
+}
- // 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; }
+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;
+}
- auto sep_pos = request.path.find(separator, starting_pos);
- if (sep_pos == std::string::npos) { sep_pos = request.path.length(); }
+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;
+}
- const auto ¶m_name = param_names_[i];
+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;
+}
- request.path_params.emplace(
- param_name, request.path.substr(starting_pos, sep_pos - starting_pos));
+void Response::set_file_content(const std::string &path,
+ const std::string &content_type) {
+ file_content_path_ = path;
+ file_content_content_type_ = content_type;
+}
- // 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();
+void Response::set_file_content(const std::string &path) {
+ file_content_path_ = path;
}
-bool RegexMatcher::match(Request &request) const {
- request.path_params.clear();
- return std::regex_match(request.path, request.matches, regex_);
+// 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);
}
-// 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 + "]";
- }
+bool Result::has_request_header(const std::string &key) const {
+ return request_headers_.find(key) != request_headers_.end();
}
-std::string make_host_and_port_string(const std::string &host, int port,
- bool is_ssl) {
- auto result = prepare_host_string(host);
+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);
+}
- // Append port if not default
- if ((!is_ssl && port == 80) || (is_ssl && port == 443)) {
- ; // do nothing
- } else {
- result += ":" + std::to_string(port);
- }
+size_t
+Result::get_request_header_value_count(const std::string &key) const {
+ auto r = request_headers_.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
+}
- return result;
+// Stream implementation
+ssize_t Stream::write(const char *ptr) {
+ return write(ptr, strlen(ptr));
}
-// 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);
+ssize_t Stream::write(const std::string &s) {
+ return write(s.data(), s.size());
}
-template <typename T>
-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;
+// BodyReader implementation
+ssize_t detail::BodyReader::read(char *buf, size_t len) {
+ if (!stream) {
+ last_error = Error::Connection;
+ return -1;
}
- return true;
-}
+ if (eof) { return 0; }
-} // namespace detail
+ if (!chunked) {
+ // Content-Length based reading
+ if (has_content_length && bytes_read >= content_length) {
+ eof = true;
+ return 0;
+ }
-// HTTP server implementation
-Server::Server()
- : new_task_queue(
- [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }) {
-#ifndef _WIN32
- signal(SIGPIPE, SIG_IGN);
-#endif
-}
+ 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);
-Server::~Server() = default;
+ 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;
+ }
-std::unique_ptr<detail::MatcherBase>
-Server::make_matcher(const std::string &pattern) {
- if (pattern.find("/:") != std::string::npos) {
- return detail::make_unique<detail::PathParamsMatcher>(pattern);
- } else {
- return detail::make_unique<detail::RegexMatcher>(pattern);
+ bytes_read += static_cast<size_t>(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;
}
-}
-Server &Server::Get(const std::string &pattern, Handler handler) {
- get_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
- return *this;
-}
+ // Chunked transfer encoding: delegate to shared decoder instance.
+ if (!chunked_decoder) { chunked_decoder.reset(new ChunkedDecoder(*stream)); }
-Server &Server::Post(const std::string &pattern, Handler handler) {
- post_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
- return *this;
-}
+ 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;
+ }
-Server &Server::Post(const std::string &pattern,
- HandlerWithContentReader handler) {
- post_handlers_for_content_reader_.emplace_back(make_matcher(pattern),
- std::move(handler));
- return *this;
-}
+ if (n == 0) {
+ // Final chunk observed. Leave trailer parsing to the caller (StreamHandle).
+ eof = true;
+ return 0;
+ }
-Server &Server::Put(const std::string &pattern, Handler handler) {
- put_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
- return *this;
+ bytes_read += static_cast<size_t>(n);
+ if (payload_max_length > 0 && bytes_read > payload_max_length) {
+ last_error = Error::ExceedMaxPayloadSize;
+ eof = true;
+ return -1;
+ }
+ return n;
}
-Server &Server::Put(const std::string &pattern,
- HandlerWithContentReader handler) {
- put_handlers_for_content_reader_.emplace_back(make_matcher(pattern),
- std::move(handler));
- return *this;
+// ThreadPool implementation
+ThreadPool::ThreadPool(size_t n, size_t mqr)
+ : shutdown_(false), max_queued_requests_(mqr) {
+ threads_.reserve(n);
+ while (n) {
+ threads_.emplace_back(worker(*this));
+ n--;
+ }
}
-Server &Server::Patch(const std::string &pattern, Handler handler) {
- patch_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
- return *this;
-}
+bool ThreadPool::enqueue(std::function<void()> fn) {
+ {
+ std::unique_lock<std::mutex> lock(mutex_);
+ if (max_queued_requests_ > 0 && jobs_.size() >= max_queued_requests_) {
+ return false;
+ }
+ jobs_.push_back(std::move(fn));
+ }
-Server &Server::Patch(const std::string &pattern,
- HandlerWithContentReader handler) {
- patch_handlers_for_content_reader_.emplace_back(make_matcher(pattern),
- std::move(handler));
- return *this;
+ cond_.notify_one();
+ return true;
}
-Server &Server::Delete(const std::string &pattern, Handler handler) {
- delete_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
- return *this;
-}
+void ThreadPool::shutdown() {
+ // Stop all worker threads...
+ {
+ std::unique_lock<std::mutex> lock(mutex_);
+ shutdown_ = true;
+ }
-Server &Server::Delete(const std::string &pattern,
- HandlerWithContentReader handler) {
- delete_handlers_for_content_reader_.emplace_back(make_matcher(pattern),
- std::move(handler));
- return *this;
-}
+ cond_.notify_all();
-Server &Server::Options(const std::string &pattern, Handler handler) {
- options_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
- return *this;
+ // Join...
+ for (auto &t : threads_) {
+ t.join();
+ }
}
-bool Server::set_base_dir(const std::string &dir,
- const std::string &mount_point) {
- return set_mount_point(mount_point, dir);
-}
+ThreadPool::worker::worker(ThreadPool &pool) : pool_(pool) {}
-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] == '/') {
- base_dirs_.push_back({std::move(mnt), dir, std::move(headers)});
- return true;
- }
- }
- return false;
-}
+void ThreadPool::worker::operator()() {
+ for (;;) {
+ std::function<void()> fn;
+ {
+ std::unique_lock<std::mutex> lock(pool_.mutex_);
-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;
+ pool_.cond_.wait(lock,
+ [&] { return !pool_.jobs_.empty() || pool_.shutdown_; });
+
+ if (pool_.shutdown_ && pool_.jobs_.empty()) { break; }
+
+ fn = pool_.jobs_.front();
+ pool_.jobs_.pop_front();
}
+
+ assert(true == static_cast<bool>(fn));
+ fn();
}
- 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;
+#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(OPENSSL_IS_BORINGSSL) && \
+ !defined(LIBRESSL_VERSION_NUMBER)
+ OPENSSL_thread_stop();
+#endif
}
-Server &Server::set_default_file_mimetype(const std::string &mime) {
- default_file_mimetype_ = mime;
- return *this;
-}
+/*
+ * Group 1 (continued): detail namespace - Stream implementations
+ */
-Server &Server::set_file_request_handler(Handler handler) {
- file_request_handler_ = std::move(handler);
- return *this;
-}
+namespace detail {
-Server &Server::set_error_handler_core(HandlerWithResponse handler,
- std::true_type) {
- error_handler_ = std::move(handler);
- return *this;
-}
+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);
-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;
-}
+ auto actual_timeout_msec =
+ (std::min)(max_timeout_msec - duration_msec, timeout_msec);
-Server &Server::set_exception_handler(ExceptionHandler handler) {
- exception_handler_ = std::move(handler);
- return *this;
-}
+ if (actual_timeout_msec < 0) { actual_timeout_msec = 0; }
-Server &Server::set_pre_routing_handler(HandlerWithResponse handler) {
- pre_routing_handler_ = std::move(handler);
- return *this;
+ actual_timeout_sec = actual_timeout_msec / 1000;
+ actual_timeout_usec = (actual_timeout_msec % 1000) * 1000;
}
-Server &Server::set_post_routing_handler(Handler handler) {
- post_routing_handler_ = std::move(handler);
- return *this;
-}
+// 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<std::chrono::steady_clock> 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) {}
-Server &Server::set_pre_request_handler(HandlerWithResponse handler) {
- pre_request_handler_ = std::move(handler);
- return *this;
-}
+SocketStream::~SocketStream() = default;
-Server &Server::set_logger(Logger logger) {
- logger_ = std::move(logger);
- return *this;
+bool SocketStream::is_readable() const {
+ return read_buff_off_ < read_buff_content_size_;
}
-Server &Server::set_error_logger(ErrorLogger error_logger) {
- error_logger_ = std::move(error_logger);
- return *this;
-}
+bool SocketStream::wait_readable() const {
+ if (max_timeout_msec_ <= 0) {
+ return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
+ }
-Server &Server::set_pre_compression_logger(Logger logger) {
- pre_compression_logger_ = std::move(logger);
- return *this;
-}
+ 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);
-Server &
-Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) {
- expect_100_continue_handler_ = std::move(handler);
- return *this;
+ return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0;
}
-Server &Server::set_address_family(int family) {
- address_family_ = family;
- return *this;
-}
-
-Server &Server::set_tcp_nodelay(bool on) {
- tcp_nodelay_ = on;
- return *this;
+bool SocketStream::wait_writable() const {
+ return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 &&
+ is_socket_alive(sock_);
}
-Server &Server::set_ipv6_v6only(bool on) {
- ipv6_v6only_ = on;
- return *this;
-}
+ssize_t SocketStream::read(char *ptr, size_t size) {
+#ifdef _WIN32
+ size =
+ (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
+#else
+ size = (std::min)(size,
+ static_cast<size_t>((std::numeric_limits<ssize_t>::max)()));
+#endif
-Server &Server::set_socket_options(SocketOptions socket_options) {
- socket_options_ = std::move(socket_options);
- return *this;
-}
+ 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<ssize_t>(size);
+ } else {
+ memcpy(ptr, read_buff_.data() + read_buff_off_, remaining_size);
+ read_buff_off_ += remaining_size;
+ return static_cast<ssize_t>(remaining_size);
+ }
+ }
-Server &Server::set_default_headers(Headers headers) {
- default_headers_ = std::move(headers);
- return *this;
-}
+ if (!wait_readable()) {
+ error_ = Error::Timeout;
+ return -1;
+ }
-Server &Server::set_header_writer(
- std::function<ssize_t(Stream &, Headers &)> const &writer) {
- header_writer_ = writer;
- return *this;
-}
+ read_buff_off_ = 0;
+ read_buff_content_size_ = 0;
-Server &
-Server::set_trusted_proxies(const std::vector<std::string> &proxies) {
- trusted_proxies_ = proxies;
- return *this;
+ 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<ssize_t>(size)) {
+ memcpy(ptr, read_buff_.data(), static_cast<size_t>(n));
+ return n;
+ } else {
+ memcpy(ptr, read_buff_.data(), size);
+ read_buff_off_ = size;
+ read_buff_content_size_ = static_cast<size_t>(n);
+ return static_cast<ssize_t>(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;
+ }
}
-Server &Server::set_keep_alive_max_count(size_t count) {
- keep_alive_max_count_ = count;
- return *this;
-}
+ssize_t SocketStream::write(const char *ptr, size_t size) {
+ if (!wait_writable()) { return -1; }
-Server &Server::set_keep_alive_timeout(time_t sec) {
- keep_alive_timeout_sec_ = sec;
- return *this;
-}
+#if defined(_WIN32) && !defined(_WIN64)
+ size =
+ (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
+#endif
-Server &Server::set_read_timeout(time_t sec, time_t usec) {
- read_timeout_sec_ = sec;
- read_timeout_usec_ = usec;
- return *this;
+ return send_socket(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS);
}
-Server &Server::set_write_timeout(time_t sec, time_t usec) {
- write_timeout_sec_ = sec;
- write_timeout_usec_ = usec;
- return *this;
+void SocketStream::get_remote_ip_and_port(std::string &ip,
+ int &port) const {
+ return detail::get_remote_ip_and_port(sock_, ip, port);
}
-Server &Server::set_idle_interval(time_t sec, time_t usec) {
- idle_interval_sec_ = sec;
- idle_interval_usec_ = usec;
- return *this;
+void SocketStream::get_local_ip_and_port(std::string &ip,
+ int &port) const {
+ return detail::get_local_ip_and_port(sock_, ip, port);
}
-Server &Server::set_payload_max_length(size_t length) {
- payload_max_length_ = length;
- return *this;
-}
+socket_t SocketStream::socket() const { return sock_; }
-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;
+time_t SocketStream::duration() const {
+ return std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::steady_clock::now() - start_time_)
+ .count();
}
-bool Server::listen_after_bind() { return listen_internal(); }
+// Buffer stream implementation
+bool BufferStream::is_readable() const { return true; }
-bool Server::listen(const std::string &host, int port,
- int socket_flags) {
- return bind_to_port(host, port, socket_flags) && listen_internal();
-}
+bool BufferStream::wait_readable() const { return true; }
-bool Server::is_running() const { return is_running_; }
+bool BufferStream::wait_writable() const { return true; }
-void Server::wait_until_ready() const {
- while (!is_running_ && !is_decommissioned) {
- std::this_thread::sleep_for(std::chrono::milliseconds{1});
- }
+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<size_t>(len_read);
+ return static_cast<ssize_t>(len_read);
}
-void Server::stop() {
- if (is_running_) {
- assert(svr_sock_ != INVALID_SOCKET);
- std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
- detail::shutdown_socket(sock);
- detail::close_socket(sock);
- }
- is_decommissioned = false;
+ssize_t BufferStream::write(const char *ptr, size_t size) {
+ buffer.append(ptr, size);
+ return static_cast<ssize_t>(size);
}
-void Server::decommission() { is_decommissioned = true; }
+void BufferStream::get_remote_ip_and_port(std::string & /*ip*/,
+ int & /*port*/) const {}
-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;
+void BufferStream::get_local_ip_and_port(std::string & /*ip*/,
+ int & /*port*/) const {}
- {
- size_t count = 0;
+socket_t BufferStream::socket() const { return 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++;
- });
+time_t BufferStream::duration() const { return 0; }
- if (count != 3) { return false; }
- }
+const std::string &BufferStream::get_buffer() const { return buffer; }
- thread_local const std::set<std::string> methods{
- "GET", "HEAD", "POST", "PUT", "DELETE",
- "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"};
+PathParamsMatcher::PathParamsMatcher(const std::string &pattern)
+ : MatcherBase(pattern) {
+ constexpr const char marker[] = "/:";
- if (methods.find(req.method) == methods.end()) {
- output_error_log(Error::InvalidHTTPMethod, &req);
- return false;
- }
+ // One past the last ending position of a path param substring
+ std::size_t last_param_end = 0;
- if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") {
- output_error_log(Error::InvalidHTTPVersion, &req);
- return false;
- }
+#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<std::string> param_name_set;
+#endif
- {
- // Skip URL fragment
- for (size_t i = 0; i < req.target.size(); i++) {
- if (req.target[i] == '#') {
- req.target.erase(i);
- break;
- }
- }
+ 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; }
- 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);
- });
- }
+ static_fragments_.push_back(
+ pattern.substr(last_param_end, marker_pos - last_param_end + 1));
- return true;
-}
+ const auto param_name_start = marker_pos + str_len(marker);
-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);
-}
+ auto sep_pos = pattern.find(separator, param_name_start);
+ if (sep_pos == std::string::npos) { sep_pos = pattern.length(); }
-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);
+ auto param_name =
+ pattern.substr(param_name_start, sep_pos - param_name_start);
- if (400 <= res.status && error_handler_ &&
- error_handler_(req, res) == HandlerResponse::Handled) {
- need_apply_ranges = true;
- }
+#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
- std::string content_type;
- std::string boundary;
- if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); }
+ param_names_.push_back(std::move(param_name));
- // 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);
+ last_param_end = sep_pos + 1;
}
- if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) &&
- !res.has_header("Content-Type")) {
- res.set_header("Content-Type", "text/plain");
+ if (last_param_end < pattern.length()) {
+ static_fragments_.push_back(pattern.substr(last_param_end));
}
+}
- if (res.body.empty() && !res.content_length_ && !res.content_provider_ &&
- !res.has_header("Content-Length")) {
- res.set_header("Content-Length", "0");
- }
+bool PathParamsMatcher::match(Request &request) const {
+ request.matches = std::smatch();
+ request.path_params.clear();
+ request.path_params.reserve(param_names_.size());
- if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) {
- res.set_header("Accept-Ranges", "bytes");
- }
+ // 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 (post_routing_handler_) { post_routing_handler_(req, res); }
+ if (starting_pos + fragment.length() > request.path.length()) {
+ return false;
+ }
- // 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; }
+ // 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;
+ }
- // Flush buffer
- auto &data = bstrm.get_buffer();
- detail::write_data(strm, data.data(), data.size());
- }
+ starting_pos += fragment.length();
- // Body
- auto ret = true;
- if (req.method != "HEAD") {
- if (!res.body.empty()) {
- if (!detail::write_data(strm, res.body.data(), res.body.size())) {
- ret = false;
- }
- } else if (res.content_provider_) {
- if (write_content_with_provider(strm, req, res, boundary, content_type)) {
- res.content_provider_success_ = true;
- } else {
- ret = false;
- }
- }
- }
+ // 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; }
- // Log
- output_log(req, res);
+ auto sep_pos = request.path.find(separator, starting_pos);
+ if (sep_pos == std::string::npos) { sep_pos = request.path.length(); }
- return ret;
+ 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
-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;
- };
+bool RegexMatcher::match(Request &request) const {
+ request.path_params.clear();
+ return std::regex_match(request.path, request.matches, regex_);
+}
- 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_);
+// 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 + "]";
+ }
+}
- 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);
- }
+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 {
- if (res.is_chunked_content_provider_) {
- auto type = detail::encoding_type(req, res);
+ result += ":" + std::to_string(port);
+ }
- std::unique_ptr<detail::compressor> compressor;
- if (type == detail::EncodingType::Gzip) {
-#ifdef CPPHTTPLIB_ZLIB_SUPPORT
- compressor = detail::make_unique<detail::gzip_compressor>();
-#endif
- } else if (type == detail::EncodingType::Brotli) {
-#ifdef CPPHTTPLIB_BROTLI_SUPPORT
- compressor = detail::make_unique<detail::brotli_compressor>();
-#endif
- } else if (type == detail::EncodingType::Zstd) {
-#ifdef CPPHTTPLIB_ZSTD_SUPPORT
- compressor = detail::make_unique<detail::zstd_compressor>();
-#endif
- } else {
- compressor = detail::make_unique<detail::nocompressor>();
- }
- assert(compressor != nullptr);
+ return result;
+}
- 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);
+// 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);
+}
+
+template <typename T>
+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;
}
-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;
- }
+} // namespace detail
- // 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;
- }
+/*
+ * Group 2 (continued): detail namespace - SSLSocketStream implementation
+ */
- 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;
- }
+#ifdef CPPHTTPLIB_SSL_ENABLED
+namespace detail {
- 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 (!content_type.find("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;
+// 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<std::chrono::steady_clock> 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<SSL *>(session), SSL_MODE_AUTO_RETRY);
+#endif
}
-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));
+SSLSocketStream::~SSLSocketStream() = default;
+
+bool SSLSocketStream::is_readable() const {
+ return tls::pending(session_) > 0;
}
-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;
+bool SSLSocketStream::wait_readable() const {
+ if (max_timeout_msec_ <= 0) {
+ return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
+ }
- 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;
- }
+ 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);
- 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); };
- }
+ return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0;
+}
- // RFC 7230 Section 3.3.3: If this is a request message and none of the above
- // are true (no Transfer-Encoding and no Content-Length), then the message
- // body length is zero (no message body is present).
- //
- // For non-SSL builds, peek into the socket to detect clients that send a
- // body without a Content-Length header (raw HTTP over TCP). If there is
- // pending data that exceeds the configured payload limit, treat this as an
- // oversized request and fail early (causing connection close). For SSL
- // builds we cannot reliably peek the decrypted application bytes, so keep
- // the original behaviour.
-#if !defined(CPPHTTPLIB_OPENSSL_SUPPORT)
- if (!req.has_header("Content-Length") &&
- !detail::is_chunked_transfer_encoding(req.headers)) {
- // Only peek if payload_max_length is set to a finite value
- if (payload_max_length_ > 0 &&
- payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
- socket_t s = strm.socket();
- if (s != INVALID_SOCKET) {
- // Peek to check if there is any pending data
- char peekbuf[1];
- ssize_t n = ::recv(s, peekbuf, 1, MSG_PEEK);
- if (n > 0) {
- // There is data, so read it with payload limit enforcement
- 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;
+bool SSLSocketStream::wait_writable() const {
+ return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 &&
+ is_socket_alive(sock_) && !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 true;
- }
-#else
- if (!req.has_header("Content-Length") &&
- !detail::is_chunked_transfer_encoding(req.headers)) {
- return true;
+ return ret;
+ } else {
+ error_ = Error::Timeout;
+ return -1;
}
-#endif
+}
- if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr,
- out, true)) {
- return false;
- }
+ssize_t SSLSocketStream::write(const char *ptr, size_t size) {
+ if (wait_writable()) {
+ auto handle_size =
+ std::min<size_t>(size, (std::numeric_limits<int>::max)());
- 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;
+ 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;
+}
- return true;
+void SSLSocketStream::get_remote_ip_and_port(std::string &ip,
+ int &port) const {
+ detail::get_remote_ip_and_port(sock_, ip, port);
}
-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"; }
+void SSLSocketStream::get_local_ip_and_port(std::string &ip,
+ int &port) const {
+ detail::get_local_ip_and_port(sock_, ip, port);
+}
- detail::FileStat stat(path);
+socket_t SSLSocketStream::socket() const { return sock_; }
- if (stat.is_dir()) {
- res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301);
- return true;
- }
+time_t SSLSocketStream::duration() const {
+ return std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::steady_clock::now() - start_time_)
+ .count();
+}
- if (stat.is_file()) {
- for (const auto &kv : entry.headers) {
- res.set_header(kv.first, kv.second);
- }
+} // namespace detail
+#endif // CPPHTTPLIB_SSL_ENABLED
- auto etag = detail::compute_etag(stat);
- if (!etag.empty()) { res.set_header("ETag", etag); }
+/*
+ * Group 4: Server implementation
+ */
- auto mtime = stat.mtime();
+// HTTP server implementation
+Server::Server()
+ : new_task_queue(
+ [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }) {
+#ifndef _WIN32
+ signal(SIGPIPE, SIG_IGN);
+#endif
+}
- auto last_modified = detail::file_mtime_to_http_date(mtime);
- if (!last_modified.empty()) {
- res.set_header("Last-Modified", last_modified);
- }
+Server::~Server() = default;
- if (check_if_not_modified(req, res, etag, mtime)) { return true; }
+std::unique_ptr<detail::MatcherBase>
+Server::make_matcher(const std::string &pattern) {
+ if (pattern.find("/:") != std::string::npos) {
+ return detail::make_unique<detail::PathParamsMatcher>(pattern);
+ } else {
+ return detail::make_unique<detail::RegexMatcher>(pattern);
+ }
+}
- check_if_range(req, etag, mtime);
+Server &Server::Get(const std::string &pattern, Handler handler) {
+ get_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
+ return *this;
+}
- auto mm = std::make_shared<detail::mmap>(path.c_str());
- if (!mm->is_open()) {
- output_error_log(Error::OpenFile, &req);
- return false;
- }
+Server &Server::Post(const std::string &pattern, Handler handler) {
+ post_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
+ return *this;
+}
- 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;
- });
+Server &Server::Post(const std::string &pattern,
+ HandlerWithContentReader handler) {
+ post_handlers_for_content_reader_.emplace_back(make_matcher(pattern),
+ std::move(handler));
+ return *this;
+}
- if (req.method != "HEAD" && file_request_handler_) {
- file_request_handler_(req, res);
- }
+Server &Server::Put(const std::string &pattern, Handler handler) {
+ put_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
+ return *this;
+}
- return true;
- } else {
- output_error_log(Error::OpenFile, &req);
- }
- }
- }
- }
- return false;
+Server &Server::Put(const std::string &pattern,
+ HandlerWithContentReader handler) {
+ put_handlers_for_content_reader_.emplace_back(make_matcher(pattern),
+ std::move(handler));
+ return *this;
}
-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");
+Server &Server::Patch(const std::string &pattern, Handler handler) {
+ patch_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
+ return *this;
+}
- // 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) {
- return std::equal(b, e, "*") ||
- std::equal(b, e, etag.begin());
- });
+Server &Server::Patch(const std::string &pattern,
+ HandlerWithContentReader handler) {
+ patch_handlers_for_content_reader_.emplace_back(make_matcher(pattern),
+ std::move(handler));
+ return *this;
+}
- 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);
+Server &Server::Delete(const std::string &pattern, Handler handler) {
+ delete_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
+ return *this;
+}
- if (t != static_cast<time_t>(-1) && mtime <= t) {
- res.status = StatusCode::NotModified_304;
+Server &Server::Delete(const std::string &pattern,
+ HandlerWithContentReader handler) {
+ delete_handlers_for_content_reader_.emplace_back(make_matcher(pattern),
+ std::move(handler));
+ return *this;
+}
+
+Server &Server::Options(const std::string &pattern, Handler handler) {
+ options_handlers_.emplace_back(make_matcher(pattern), std::move(handler));
+ 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] == '/') {
+ base_dirs_.push_back({std::move(mnt), dir, std::move(headers)});
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<time_t>(-1) && mtime <= t);
- }
- };
-
- if (!is_valid_range()) {
- // Validator doesn't match: ignore Range and serve full content
- req.ranges.clear();
- 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;
+}
- return true;
+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;
}
-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<socklen_t>(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;
- });
+Server &Server::set_default_file_mimetype(const std::string &mime) {
+ default_file_mimetype_ = mime;
+ return *this;
}
-int Server::bind_internal(const std::string &host, int port,
- int socket_flags) {
- if (is_decommissioned) { return -1; }
+Server &Server::set_file_request_handler(Handler handler) {
+ file_request_handler_ = std::move(handler);
+ return *this;
+}
- if (!is_valid()) { return -1; }
+Server &Server::set_error_handler_core(HandlerWithResponse handler,
+ std::true_type) {
+ error_handler_ = std::move(handler);
+ return *this;
+}
- svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_);
- if (svr_sock_ == INVALID_SOCKET) { return -1; }
+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;
+}
- if (port == 0) {
- struct sockaddr_storage addr;
- socklen_t addr_len = sizeof(addr);
- if (getsockname(svr_sock_, reinterpret_cast<struct sockaddr *>(&addr),
- &addr_len) == -1) {
- output_error_log(Error::GetSockName, nullptr);
- return -1;
- }
- if (addr.ss_family == AF_INET) {
- return ntohs(reinterpret_cast<struct sockaddr_in *>(&addr)->sin_port);
- } else if (addr.ss_family == AF_INET6) {
- return ntohs(reinterpret_cast<struct sockaddr_in6 *>(&addr)->sin6_port);
- } else {
- output_error_log(Error::UnsupportedAddressFamily, nullptr);
- return -1;
- }
- } else {
- return port;
- }
+Server &Server::set_exception_handler(ExceptionHandler handler) {
+ exception_handler_ = std::move(handler);
+ return *this;
}
-bool Server::listen_internal() {
- if (is_decommissioned) { return false; }
+Server &Server::set_pre_routing_handler(HandlerWithResponse handler) {
+ pre_routing_handler_ = std::move(handler);
+ return *this;
+}
- auto ret = true;
- is_running_ = true;
- auto se = detail::scope_exit([&]() { is_running_ = false; });
+Server &Server::set_post_routing_handler(Handler handler) {
+ post_routing_handler_ = std::move(handler);
+ return *this;
+}
- {
- std::unique_ptr<TaskQueue> task_queue(new_task_queue());
+Server &Server::set_pre_request_handler(HandlerWithResponse handler) {
+ pre_request_handler_ = std::move(handler);
+ return *this;
+}
- 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
+Server &Server::set_logger(Logger logger) {
+ logger_ = std::move(logger);
+ return *this;
+}
-#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
+Server &Server::set_error_logger(ErrorLogger error_logger) {
+ error_logger_ = std::move(error_logger);
+ return *this;
+}
- 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;
- }
+Server &Server::set_pre_compression_logger(Logger logger) {
+ pre_compression_logger_ = std::move(logger);
+ return *this;
+}
- 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_);
+Server &
+Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) {
+ expect_100_continue_handler_ = std::move(handler);
+ return *this;
+}
- 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();
- }
+Server &Server::set_address_family(int family) {
+ address_family_ = family;
+ return *this;
+}
- is_decommissioned = !ret;
- return ret;
+Server &Server::set_tcp_nodelay(bool on) {
+ tcp_nodelay_ = on;
+ return *this;
}
-bool Server::routing(Request &req, Response &res, Stream &strm) {
- if (pre_routing_handler_ &&
- pre_routing_handler_(req, res) == HandlerResponse::Handled) {
- return true;
- }
+Server &Server::set_ipv6_v6only(bool on) {
+ ipv6_v6only_ = on;
+ return *this;
+}
- // File handler
- if ((req.method == "GET" || req.method == "HEAD") &&
- handle_file_request(req, res)) {
- return true;
- }
+Server &Server::set_socket_options(SocketOptions socket_options) {
+ socket_options_ = std::move(socket_options);
+ return *this;
+}
- if (detail::expect_content(req)) {
- // Content reader handler
- {
- 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); }
- 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); }
- return result;
- });
+Server &Server::set_default_headers(Headers headers) {
+ default_headers_ = std::move(headers);
+ return *this;
+}
- if (req.method == "POST") {
- if (dispatch_request_for_content_reader(
- req, res, std::move(reader),
- post_handlers_for_content_reader_)) {
- return true;
- }
- } else if (req.method == "PUT") {
- if (dispatch_request_for_content_reader(
- req, res, std::move(reader),
- put_handlers_for_content_reader_)) {
- return true;
- }
- } else if (req.method == "PATCH") {
- if (dispatch_request_for_content_reader(
- req, res, std::move(reader),
- patch_handlers_for_content_reader_)) {
- return true;
- }
- } else if (req.method == "DELETE") {
- if (dispatch_request_for_content_reader(
- req, res, std::move(reader),
- delete_handlers_for_content_reader_)) {
- return true;
- }
- }
- }
+Server &Server::set_header_writer(
+ std::function<ssize_t(Stream &, Headers &)> const &writer) {
+ header_writer_ = writer;
+ return *this;
+}
- // Read content into `req.body`
- if (!read_content(strm, req, res)) {
- output_error_log(Error::Read, &req);
- return false;
- }
- }
+Server &
+Server::set_trusted_proxies(const std::vector<std::string> &proxies) {
+ trusted_proxies_ = proxies;
+ return *this;
+}
- // Regular handler
- if (req.method == "GET" || req.method == "HEAD") {
- return dispatch_request(req, res, get_handlers_);
- } else if (req.method == "POST") {
- return dispatch_request(req, res, post_handlers_);
- } else if (req.method == "PUT") {
- return dispatch_request(req, res, put_handlers_);
- } else if (req.method == "DELETE") {
- return dispatch_request(req, res, delete_handlers_);
- } else if (req.method == "OPTIONS") {
- return dispatch_request(req, res, options_handlers_);
- } else if (req.method == "PATCH") {
- return dispatch_request(req, res, patch_handlers_);
- }
+Server &Server::set_keep_alive_max_count(size_t count) {
+ keep_alive_max_count_ = count;
+ return *this;
+}
- res.status = StatusCode::BadRequest_400;
- return false;
+Server &Server::set_keep_alive_timeout(time_t sec) {
+ keep_alive_timeout_sec_ = sec;
+ return *this;
}
-bool Server::dispatch_request(Request &req, Response &res,
- const Handlers &handlers) const {
- for (const auto &x : handlers) {
- const auto &matcher = x.first;
- const auto &handler = x.second;
+Server &Server::set_read_timeout(time_t sec, time_t usec) {
+ read_timeout_sec_ = sec;
+ read_timeout_usec_ = usec;
+ return *this;
+}
- if (matcher->match(req)) {
- req.matched_route = matcher->pattern();
- if (!pre_request_handler_ ||
- pre_request_handler_(req, res) != HandlerResponse::Handled) {
- handler(req, res);
- }
- return true;
- }
- }
- return false;
+Server &Server::set_write_timeout(time_t sec, time_t usec) {
+ write_timeout_sec_ = sec;
+ write_timeout_usec_ = usec;
+ return *this;
}
-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);
- }
+Server &Server::set_idle_interval(time_t sec, time_t usec) {
+ idle_interval_sec_ = sec;
+ idle_interval_usec_ = usec;
+ return *this;
+}
- boundary = detail::make_multipart_data_boundary();
+Server &Server::set_payload_max_length(size_t length) {
+ payload_max_length_ = length;
+ return *this;
+}
- res.set_header("Content-Type",
- "multipart/byteranges; boundary=" + boundary);
- }
+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;
+}
- auto type = detail::encoding_type(req, res);
+bool Server::listen_after_bind() { return listen_internal(); }
- 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_);
+bool Server::listen(const std::string &host, int port,
+ int socket_flags) {
+ return bind_to_port(host, port, socket_flags) && listen_internal();
+}
- length = offset_and_length.second;
+bool Server::is_running() const { return is_running_; }
- 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::Gzip) {
- res.set_header("Content-Encoding", "gzip");
- res.set_header("Vary", "Accept-Encoding");
- } else if (type == detail::EncodingType::Brotli) {
- res.set_header("Content-Encoding", "br");
- res.set_header("Vary", "Accept-Encoding");
- } else if (type == detail::EncodingType::Zstd) {
- res.set_header("Content-Encoding", "zstd");
- 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;
+void Server::wait_until_ready() const {
+ while (!is_running_ && !is_decommissioned) {
+ std::this_thread::sleep_for(std::chrono::milliseconds{1});
+ }
+}
- auto content_range = detail::make_content_range_header_field(
- offset_and_length, res.body.size());
- res.set_header("Content-Range", content_range);
+void Server::stop() {
+ if (is_running_) {
+ assert(svr_sock_ != INVALID_SOCKET);
+ std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
+ detail::shutdown_socket(sock);
+ detail::close_socket(sock);
+ }
+ is_decommissioned = false;
+}
- 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);
- }
+void Server::decommission() { is_decommissioned = true; }
- if (type != detail::EncodingType::None) {
- output_pre_compression_log(req, res);
+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;
- std::unique_ptr<detail::compressor> compressor;
- std::string content_encoding;
-
- if (type == detail::EncodingType::Gzip) {
-#ifdef CPPHTTPLIB_ZLIB_SUPPORT
- compressor = detail::make_unique<detail::gzip_compressor>();
- content_encoding = "gzip";
-#endif
- } else if (type == detail::EncodingType::Brotli) {
-#ifdef CPPHTTPLIB_BROTLI_SUPPORT
- compressor = detail::make_unique<detail::brotli_compressor>();
- content_encoding = "br";
-#endif
- } else if (type == detail::EncodingType::Zstd) {
-#ifdef CPPHTTPLIB_ZSTD_SUPPORT
- compressor = detail::make_unique<detail::zstd_compressor>();
- content_encoding = "zstd";
-#endif
- }
+ {
+ size_t count = 0;
- if (compressor) {
- 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", content_encoding);
- res.set_header("Vary", "Accept-Encoding");
- }
+ 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++;
+ });
- auto length = std::to_string(res.body.size());
- res.set_header("Content-Length", length);
+ if (count != 3) { return false; }
}
-}
-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;
+ thread_local const std::set<std::string> methods{
+ "GET", "HEAD", "POST", "PUT", "DELETE",
+ "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"};
- 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;
- }
+ if (methods.find(req.method) == methods.end()) {
+ output_error_log(Error::InvalidHTTPMethod, &req);
+ return false;
}
- return false;
-}
-
-std::string
-get_client_ip(const std::string &x_forwarded_for,
- const std::vector<std::string> &trusted_proxies) {
- // X-Forwarded-For is a comma-separated list per RFC 7239
- std::vector<std::string> 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<size_t>(e - b));
- ip_list.emplace_back(std::string(b + r.first, b + r.second));
- });
-
- 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 (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") {
+ output_error_log(Error::InvalidHTTPVersion, &req);
+ return false;
+ }
- 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];
+ {
+ // 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);
+ });
}
- // If no trusted proxy is found, return the first IP in the list
- return ip_list.front();
+ return true;
}
-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<void(Request &)> &setup_request) {
- std::array<char, 2048> buf{};
-
- detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
-
- // Connection has been closed on client
- if (!line_reader.getline()) { return false; }
+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);
+}
- 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;
+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);
+}
- Response res;
- res.version = "HTTP/1.1";
- res.headers = default_headers_;
+bool Server::write_response_core(Stream &strm, bool close_connection,
+ const Request &req, Response &res,
+ bool need_apply_ranges) {
+ assert(res.status != -1);
-#ifdef __APPLE__
- // Socket file descriptor exceeded FD_SETSIZE...
- if (strm.socket() >= FD_SETSIZE) {
- Headers dummy;
- detail::read_headers(strm, dummy);
- res.status = StatusCode::InternalServerError_500;
- output_error_log(Error::ExceedMaxSocketDescriptorCount, &req);
- return write_response(strm, close_connection, req, res);
+ if (400 <= res.status && error_handler_ &&
+ error_handler_(req, res) == HandlerResponse::Handled) {
+ need_apply_ranges = true;
}
-#endif
- // 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);
- }
+ std::string content_type;
+ std::string boundary;
+ if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); }
- // 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);
+ // 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);
}
- // Check if the request URI doesn't exceed the limit
- if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
- res.status = StatusCode::UriTooLong_414;
- output_error_log(Error::ExceedUriMaxLength, &req);
- return write_response(strm, close_connection, req, res);
+ if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) &&
+ !res.has_header("Content-Type")) {
+ res.set_header("Content-Type", "text/plain");
}
- if (req.get_header_value("Connection") == "close") {
- connection_closed = true;
+ if (res.body.empty() && !res.content_length_ && !res.content_provider_ &&
+ !res.has_header("Content-Length")) {
+ res.set_header("Content-Length", "0");
}
- if (req.version == "HTTP/1.0" &&
- req.get_header_value("Connection") != "Keep-Alive") {
- connection_closed = true;
+ if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) {
+ res.set_header("Accept-Ranges", "bytes");
}
- if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) {
- auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
- req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_);
- } else {
- req.remote_addr = remote_addr;
- }
- req.remote_port = remote_port;
+ if (post_routing_handler_) { post_routing_handler_(req, res); }
- req.local_addr = local_addr;
- req.local_port = local_port;
+ // 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; }
- 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)) {
- res.status = StatusCode::BadRequest_400;
- output_error_log(Error::HTTPParsing, &req);
- return write_response(strm, close_connection, req, res);
- }
+ // Flush buffer
+ auto &data = bstrm.get_buffer();
+ detail::write_data(strm, data.data(), data.size());
}
- 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)) {
- res.status = StatusCode::RangeNotSatisfiable_416;
- output_error_log(Error::InvalidRangeHeader, &req);
- return write_response(strm, close_connection, req, res);
+ // Body
+ auto ret = true;
+ if (req.method != "HEAD") {
+ if (!res.body.empty()) {
+ if (!detail::write_data(strm, res.body.data(), res.body.size())) {
+ ret = false;
+ }
+ } else if (res.content_provider_) {
+ if (write_content_with_provider(strm, req, res, boundary, content_type)) {
+ res.content_provider_success_ = true;
+ } else {
+ ret = false;
+ }
}
}
- if (setup_request) { setup_request(req); }
+ // Log
+ output_log(req, res);
- 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);
- }
- }
+ return ret;
+}
- // Setup `is_connection_closed` method
- auto sock = strm.socket();
- req.is_connection_closed = [sock]() {
- return !detail::is_socket_alive(sock);
+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;
};
- // Routing
- auto routed = false;
-#ifdef CPPHTTPLIB_NO_EXCEPTIONS
- routed = routing(req, res, strm);
-#else
- try {
- routed = routing(req, res, strm);
- } catch (std::exception &e) {
- if (exception_handler_) {
- auto ep = std::current_exception();
- exception_handler_(req, res, ep);
- routed = true;
+ 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 {
- res.status = StatusCode::InternalServerError_500;
- std::string val;
- auto s = e.what();
- for (size_t i = 0; s[i]; i++) {
- switch (s[i]) {
- case '\r': val += "\\r"; break;
- case '\n': val += "\\n"; break;
- default: val += s[i]; break;
- }
- }
- res.set_header("EXCEPTION_WHAT", val);
+ return detail::write_multipart_ranges_data(
+ strm, req, res, boundary, content_type, res.content_length_,
+ is_shutting_down);
}
- } catch (...) {
- if (exception_handler_) {
- auto ep = std::current_exception();
- exception_handler_(req, res, ep);
- routed = true;
+ } else {
+ if (res.is_chunked_content_provider_) {
+ auto type = detail::encoding_type(req, res);
+
+ std::unique_ptr<detail::compressor> compressor;
+ if (type == detail::EncodingType::Gzip) {
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+ compressor = detail::make_unique<detail::gzip_compressor>();
+#endif
+ } else if (type == detail::EncodingType::Brotli) {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+ compressor = detail::make_unique<detail::brotli_compressor>();
+#endif
+ } else if (type == detail::EncodingType::Zstd) {
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+ compressor = detail::make_unique<detail::zstd_compressor>();
+#endif
+ } else {
+ compressor = detail::make_unique<detail::nocompressor>();
+ }
+ assert(compressor != nullptr);
+
+ return detail::write_content_chunked(strm, res.content_provider_,
+ is_shutting_down, *compressor);
} else {
- res.status = StatusCode::InternalServerError_500;
- res.set_header("EXCEPTION_WHAT", "UNKNOWN");
+ return detail::write_content_without_length(strm, res.content_provider_,
+ is_shutting_down);
}
}
-#endif
- 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
- if (!res.file_content_path_.empty()) {
- const auto &path = res.file_content_path_;
- auto mm = std::make_shared<detail::mmap>(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);
- return write_response(strm, close_connection, req, res);
- }
+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;
+ }
- 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_);
- }
+ // 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;
+ }
- res.set_content_provider(
- mm->size(), content_type,
- [mm](size_t offset, size_t length, DataSink &sink) -> bool {
- sink.write(mm->data() + offset, length);
+ 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 (detail::range_error(req, res)) {
- res.body.clear();
- res.content_length_ = 0;
- res.content_provider_ = nullptr;
- res.status = StatusCode::RangeNotSatisfiable_416;
- return write_response(strm, close_connection, req, res);
+ 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 (!content_type.find("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 write_response_with_content(strm, close_connection, req, res);
- } else {
- if (res.status == -1) { res.status = StatusCode::NotFound_404; }
-
- return write_response(strm, close_connection, req, res);
+ return true;
}
+ return false;
}
-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);
+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));
+}
- std::string local_addr;
- int local_port = 0;
- detail::get_local_ip_and_port(sock, local_addr, local_port);
+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;
- 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);
- });
+ 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;
+ }
- detail::shutdown_socket(sock);
- detail::close_socket(sock);
- return ret;
-}
+ 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); };
+ }
-void Server::output_log(const Request &req, const Response &res) const {
- if (logger_) {
- std::lock_guard<std::mutex> guard(logger_mutex_);
- logger_(req, res);
+ // RFC 7230 Section 3.3.3: If this is a request message and none of the above
+ // are true (no Transfer-Encoding and no Content-Length), then the message
+ // body length is zero (no message body is present).
+ //
+ // For non-SSL builds, detect clients that send a body without a
+ // Content-Length header (raw HTTP over TCP). Check both the stream's
+ // internal read buffer (data already read from the socket during header
+ // parsing) and the socket itself for pending data. If data is found and
+ // exceeds the configured payload limit, reject with 413.
+ // For SSL builds we cannot reliably peek the decrypted application bytes,
+ // so keep the original behaviour.
+#if !defined(CPPHTTPLIB_SSL_ENABLED)
+ if (!req.has_header("Content-Length") &&
+ !detail::is_chunked_transfer_encoding(req.headers)) {
+ // Only check if payload_max_length is set to a finite value
+ if (payload_max_length_ > 0 &&
+ payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
+ // Check if there is data already buffered in the stream (read during
+ // header parsing) or pending on the socket. Use a non-blocking socket
+ // check to avoid deadlock when the client sends no body.
+ bool has_data = strm.is_readable();
+ if (!has_data) {
+ socket_t 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;
}
-}
-
-void Server::output_pre_compression_log(const Request &req,
- const Response &res) const {
- if (pre_compression_logger_) {
- std::lock_guard<std::mutex> guard(logger_mutex_);
- pre_compression_logger_(req, res);
+#else
+ if (!req.has_header("Content-Length") &&
+ !detail::is_chunked_transfer_encoding(req.headers)) {
+ return true;
}
-}
+#endif
-void Server::output_error_log(const Error &err,
- const Request *req) const {
- if (error_logger_) {
- std::lock_guard<std::mutex> guard(logger_mutex_);
- error_logger_(err, req);
+ if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr,
+ out, true)) {
+ return false;
}
-}
-
-// 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<std::mutex> guard(socket_mutex_);
- if (socket_requests_in_flight_ == 0) { break; }
+ 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;
}
- std::this_thread::sleep_for(std::chrono::milliseconds{1});
}
- std::lock_guard<std::mutex> guard(socket_mutex_);
- shutdown_socket(socket_);
- close_socket(socket_);
+ return true;
}
-bool ClientImpl::is_valid() const { 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"; }
-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_;
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- digest_auth_username_ = rhs.digest_auth_username_;
- digest_auth_password_ = rhs.digest_auth_password_;
-#endif
- 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_;
- 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_;
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_;
- proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_;
-#endif
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- ca_cert_file_path_ = rhs.ca_cert_file_path_;
- ca_cert_dir_path_ = rhs.ca_cert_dir_path_;
- ca_cert_store_ = rhs.ca_cert_store_;
-#endif
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- server_certificate_verification_ = rhs.server_certificate_verification_;
- server_hostname_verification_ = rhs.server_hostname_verification_;
- server_certificate_verifier_ = rhs.server_certificate_verifier_;
-#endif
- logger_ = rhs.logger_;
- error_logger_ = rhs.error_logger_;
-}
+ detail::FileStat stat(path);
-socket_t ClientImpl::create_client_socket(Error &error) const {
- if (!proxy_host_.empty() && proxy_port_ != -1) {
- 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);
- }
+ if (stat.is_dir()) {
+ res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301);
+ return true;
+ }
- // Check is custom IP specified for host_
- std::string ip;
- auto it = addr_map_.find(host_);
- if (it != addr_map_.end()) { ip = it->second; }
+ if (stat.is_file()) {
+ for (const auto &kv : entry.headers) {
+ res.set_header(kv.first, kv.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);
-}
+ auto etag = detail::compute_etag(stat);
+ if (!etag.empty()) { res.set_header("ETag", etag); }
-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;
-}
+ auto mtime = stat.mtime();
-bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
- return create_and_connect_socket(socket, error);
-}
+ auto last_modified = detail::file_mtime_to_http_date(mtime);
+ if (!last_modified.empty()) {
+ res.set_header("Last-Modified", last_modified);
+ }
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) {
- if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; }
+ if (check_if_not_modified(req, res, etag, mtime)) { return true; }
- if (!proxy_host_.empty() && proxy_port_ != -1) { return true; }
+ check_if_range(req, etag, mtime);
- if (!initialize_ssl(socket, error)) {
- shutdown_socket(socket);
- close_socket(socket);
- return false;
- }
+ auto mm = std::make_shared<detail::mmap>(path.c_str());
+ if (!mm->is_open()) {
+ output_error_log(Error::OpenFile, &req);
+ return false;
+ }
- return true;
-}
-#endif
+ 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;
+ });
-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());
-}
+ if (req.method != "HEAD" && file_request_handler_) {
+ file_request_handler_(req, res);
+ }
-void ClientImpl::shutdown_socket(Socket &socket) const {
- if (socket.sock == INVALID_SOCKET) { return; }
- detail::shutdown_socket(socket.sock);
+ return true;
+ } else {
+ output_error_log(Error::OpenFile, &req);
+ }
+ }
+ }
+ }
+ return false;
}
-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());
+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");
- // It is also a bug if this happens while SSL is still active
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- assert(socket.ssl == nullptr);
-#endif
- if (socket.sock == INVALID_SOCKET) { return; }
- detail::close_socket(socket.sock);
- socket.sock = INVALID_SOCKET;
-}
+ // 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<size_t>(e - b);
+ return (seg_len == 1 && *b == '*') ||
+ (seg_len == etag.size() &&
+ std::equal(b, e, etag.begin()));
+ });
-bool ClientImpl::read_response_line(Stream &strm, const Request &req,
- Response &res,
- bool skip_100_continue) const {
- std::array<char, 2048> 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
+ 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);
- std::cmatch m;
- if (!std::regex_match(line_reader.ptr(), m, re)) {
- return req.method == "CONNECT";
+ if (t != static_cast<time_t>(-1) && mtime <= t) {
+ res.status = StatusCode::NotModified_304;
+ return true;
+ }
}
- res.version = std::string(m[1]);
- res.status = std::stoi(std::string(m[2]));
- res.reason = std::string(m[3]);
+ return false;
+}
- // 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
+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");
- 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]);
+ 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<time_t>(-1) && mtime <= t);
+ }
+ };
+
+ if (!is_valid_range()) {
+ // Validator doesn't match: ignore Range and serve full content
+ req.ranges.clear();
+ return false;
+ }
}
return true;
}
-bool ClientImpl::send(Request &req, Response &res, Error &error) {
- std::lock_guard<std::recursive_mutex> request_mutex_guard(request_mutex_);
- auto ret = send_(req, res, error);
- if (error == Error::SSLPeerCouldBeClosed_) {
- assert(!ret);
- ret = send_(req, res, error);
+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<socklen_t>(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<struct sockaddr *>(&addr),
+ &addr_len) == -1) {
+ output_error_log(Error::GetSockName, nullptr);
+ return -1;
+ }
+ if (addr.ss_family == AF_INET) {
+ return ntohs(reinterpret_cast<struct sockaddr_in *>(&addr)->sin_port);
+ } else if (addr.ss_family == AF_INET6) {
+ return ntohs(reinterpret_cast<struct sockaddr_in6 *>(&addr)->sin6_port);
+ } else {
+ output_error_log(Error::UnsupportedAddressFamily, nullptr);
+ return -1;
+ }
+ } else {
+ return port;
}
- return ret;
}
-bool ClientImpl::send_(Request &req, Response &res, Error &error) {
- {
- std::lock_guard<std::mutex> guard(socket_mutex_);
+bool Server::listen_internal() {
+ if (is_decommissioned) { return false; }
- // 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 ret = true;
+ is_running_ = true;
+ auto se = detail::scope_exit([&]() { is_running_ = false; });
- auto is_alive = false;
- if (socket_.is_open()) {
- is_alive = detail::is_socket_alive(socket_.sock);
+ {
+ std::unique_ptr<TaskQueue> task_queue(new_task_queue());
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- if (is_alive && is_ssl()) {
- if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) {
- is_alive = false;
+ 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 (!is_alive) {
- // Attempt to avoid sigpipe by shutting down non-gracefully if it
- // seems like the other side has already closed the connection Also,
- // there cannot be any requests in flight from other threads since we
- // locked request_mutex_, so safe to close everything immediately
- const bool shutdown_gracefully = false;
- shutdown_ssl(socket_, shutdown_gracefully);
- shutdown_socket(socket_);
- close_socket(socket_);
- }
- }
+#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 (!is_alive) {
- if (!ensure_socket_connection(socket_, error)) {
- output_error_log(error, &req);
- return false;
+ 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;
}
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- // TODO: refactoring
- if (is_ssl()) {
- auto &scli = static_cast<SSLClient &>(*this);
- if (!proxy_host_.empty() && proxy_port_ != -1) {
- auto success = false;
- if (!scli.connect_with_proxy(socket_, req.start_time_, res, success,
- error)) {
- if (!success) { output_error_log(error, &req); }
- return success;
- }
- }
+ 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 (!proxy_host_.empty() && proxy_port_ != -1) {
- if (!scli.initialize_ssl(socket_, error)) {
- output_error_log(error, &req);
- return false;
- }
- }
+ 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);
}
-#endif
}
- // 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();
+ task_queue->shutdown();
}
- for (const auto &header : default_headers_) {
- if (req.headers.find(header.first) == req.headers.end()) {
- req.headers.insert(header);
- }
+ 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;
}
- auto ret = false;
- auto close_connection = !keep_alive_;
+ // File handler
+ if ((req.method == "GET" || req.method == "HEAD") &&
+ handle_file_request(req, res)) {
+ return true;
+ }
- auto se = detail::scope_exit([&]() {
- // Briefly lock mutex in order to mark that a request is no longer ongoing
- std::lock_guard<std::mutex> 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 (detail::expect_content(req)) {
+ // Content reader handler
+ {
+ 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); }
+ 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); }
+ return result;
+ });
- if (socket_should_be_closed_when_request_is_done_ || close_connection ||
- !ret) {
- shutdown_ssl(socket_, true);
- shutdown_socket(socket_);
- close_socket(socket_);
+ if (req.method == "POST") {
+ if (dispatch_request_for_content_reader(
+ req, res, std::move(reader),
+ post_handlers_for_content_reader_)) {
+ return true;
+ }
+ } else if (req.method == "PUT") {
+ if (dispatch_request_for_content_reader(
+ req, res, std::move(reader),
+ put_handlers_for_content_reader_)) {
+ return true;
+ }
+ } else if (req.method == "PATCH") {
+ if (dispatch_request_for_content_reader(
+ req, res, std::move(reader),
+ patch_handlers_for_content_reader_)) {
+ return true;
+ }
+ } else if (req.method == "DELETE") {
+ if (dispatch_request_for_content_reader(
+ req, res, std::move(reader),
+ delete_handlers_for_content_reader_)) {
+ return 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);
+ // Read content into `req.body`
+ if (!read_content(strm, req, res)) {
+ output_error_log(Error::Read, &req);
+ return false;
}
}
- return ret;
-}
-
-Result ClientImpl::send(const Request &req) {
- auto req2 = req;
- return send_(std::move(req2));
-}
+ // Regular handler
+ if (req.method == "GET" || req.method == "HEAD") {
+ return dispatch_request(req, res, get_handlers_);
+ } else if (req.method == "POST") {
+ return dispatch_request(req, res, post_handlers_);
+ } else if (req.method == "PUT") {
+ return dispatch_request(req, res, put_handlers_);
+ } else if (req.method == "DELETE") {
+ return dispatch_request(req, res, delete_handlers_);
+ } else if (req.method == "OPTIONS") {
+ return dispatch_request(req, res, options_handlers_);
+ } else if (req.method == "PATCH") {
+ return dispatch_request(req, res, patch_handlers_);
+ }
-Result ClientImpl::send_(Request &&req) {
- auto res = detail::make_unique<Response>();
- auto error = Error::Success;
- auto ret = send(req, *res, error);
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers),
- last_ssl_error_, last_openssl_error_};
-#else
- return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)};
-#endif
+ res.status = StatusCode::BadRequest_400;
+ return false;
}
-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); }
- }
+bool Server::dispatch_request(Request &req, Response &res,
+ const Handlers &handlers) const {
+ for (const auto &x : handlers) {
+ const auto &matcher = x.first;
+ const auto &handler = x.second;
- 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 (matcher->match(req)) {
+ req.matched_route = matcher->pattern();
+ if (!pre_request_handler_ ||
+ pre_request_handler_(req, res) != HandlerResponse::Handled) {
+ handler(req, res);
+ }
+ return true;
}
}
+ return false;
+}
- 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);
+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);
}
-#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
- }
+ boundary = detail::make_multipart_data_boundary();
- 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()));
- }
+ res.set_header("Content-Type",
+ "multipart/byteranges; boundary=" + boundary);
}
-}
-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<Response>();
- handle.error = Error::Success;
+ auto type = detail::encoding_type(req, res);
- auto query_path = params.empty() ? path : append_query_params(path, params);
- handle.connection_ = detail::make_unique<ClientConnection>();
+ 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_);
- {
- std::lock_guard<std::mutex> guard(socket_mutex_);
+ length = offset_and_length.second;
- auto is_alive = false;
- if (socket_.is_open()) {
- is_alive = detail::is_socket_alive(socket_.sock);
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- if (is_alive && is_ssl()) {
- if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) {
- is_alive = false;
- }
+ 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_);
}
-#endif
- if (!is_alive) {
- shutdown_ssl(socket_, false);
- shutdown_socket(socket_);
- close_socket(socket_);
+ 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::Gzip) {
+ res.set_header("Content-Encoding", "gzip");
+ res.set_header("Vary", "Accept-Encoding");
+ } else if (type == detail::EncodingType::Brotli) {
+ res.set_header("Content-Encoding", "br");
+ res.set_header("Vary", "Accept-Encoding");
+ } else if (type == detail::EncodingType::Zstd) {
+ res.set_header("Content-Encoding", "zstd");
+ 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;
- if (!is_alive) {
- if (!ensure_socket_connection(socket_, handle.error)) {
- handle.response.reset();
- return handle;
- }
+ auto content_range = detail::make_content_range_header_field(
+ offset_and_length, res.body.size());
+ res.set_header("Content-Range", content_range);
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- if (is_ssl()) {
- auto &scli = static_cast<SSLClient &>(*this);
- if (!proxy_host_.empty() && proxy_port_ != -1) {
- if (!scli.initialize_ssl(socket_, handle.error)) {
- handle.response.reset();
- return handle;
- }
- }
- }
-#endif
+ 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);
}
- transfer_socket_ownership_to_handle(handle);
- }
+ if (type != detail::EncodingType::None) {
+ output_pre_compression_log(req, res);
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- if (is_ssl() && handle.connection_->ssl) {
- handle.socket_stream_ = detail::make_unique<detail::SSLSocketStream>(
- handle.connection_->sock, handle.connection_->ssl, read_timeout_sec_,
- read_timeout_usec_, write_timeout_sec_, write_timeout_usec_);
- } else {
- handle.socket_stream_ = detail::make_unique<detail::SocketStream>(
- handle.connection_->sock, read_timeout_sec_, read_timeout_usec_,
- write_timeout_sec_, write_timeout_usec_);
- }
-#else
- handle.socket_stream_ = detail::make_unique<detail::SocketStream>(
- 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;
- }
+ std::unique_ptr<detail::compressor> compressor;
+ std::string content_encoding;
- if (!detail::check_and_write_headers(strm, req.headers, header_writer_,
- handle.error)) {
- handle.response.reset();
- return handle;
- }
+ if (type == detail::EncodingType::Gzip) {
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+ compressor = detail::make_unique<detail::gzip_compressor>();
+ content_encoding = "gzip";
+#endif
+ } else if (type == detail::EncodingType::Brotli) {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+ compressor = detail::make_unique<detail::brotli_compressor>();
+ content_encoding = "br";
+#endif
+ } else if (type == detail::EncodingType::Zstd) {
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+ compressor = detail::make_unique<detail::zstd_compressor>();
+ content_encoding = "zstd";
+#endif
+ }
- if (!body.empty()) {
- if (strm.write(body.data(), body.size()) < 0) {
- handle.error = Error::Write;
- handle.response.reset();
- return handle;
+ if (compressor) {
+ 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", content_encoding);
+ res.set_header("Vary", "Accept-Encoding");
+ }
+ }
}
- }
-
- 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_;
-
- auto content_length_str = handle.response->get_header_value("Content-Length");
- if (!content_length_str.empty()) {
- handle.body_reader_.content_length =
- static_cast<size_t>(std::stoull(content_length_str));
+ auto length = std::to_string(res.body.size());
+ res.set_header("Content-Length", length);
}
+}
- auto transfer_encoding =
- handle.response->get_header_value("Transfer-Encoding");
- handle.body_reader_.chunked = (transfer_encoding == "chunked");
+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;
- auto content_encoding = handle.response->get_header_value("Content-Encoding");
- if (!content_encoding.empty()) {
- handle.decompressor_ = detail::create_decompressor(content_encoding);
+ 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 handle;
+ return false;
}
-ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) {
- if (!is_valid() || !response) { return -1; }
+std::string
+get_client_ip(const std::string &x_forwarded_for,
+ const std::vector<std::string> &trusted_proxies) {
+ // X-Forwarded-For is a comma-separated list per RFC 7239
+ std::vector<std::string> 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<size_t>(e - b));
+ ip_list.emplace_back(std::string(b + r.first, b + r.second));
+ });
- if (decompressor_) { return read_with_decompression(buf, len); }
- auto n = detail::read_body_content(stream_, body_reader_, buf, len);
+ for (size_t i = 0; i < ip_list.size(); ++i) {
+ auto ip = ip_list[i];
- 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;
+ 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];
}
}
}
- return n;
+ // If no trusted proxy is found, return the first IP in the list
+ return ip_list.front();
}
-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;
- return static_cast<ssize_t>(to_copy);
- }
-
- decompress_buffer_.clear();
- decompress_offset_ = 0;
-
- constexpr size_t kDecompressionBufferSize = 8192;
- char compressed_buf[kDecompressionBufferSize];
+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<void(Request &)> &setup_request) {
+ std::array<char, 2048> buf{};
- while (true) {
- auto n = detail::read_body_content(stream_, body_reader_, compressed_buf,
- sizeof(compressed_buf));
+ detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
- if (n <= 0) { return n; }
+ // Connection has been closed on client
+ if (!line_reader.getline()) { return false; }
- bool decompress_ok =
- decompressor_->decompress(compressed_buf, static_cast<size_t>(n),
- [this](const char *data, size_t data_len) {
- decompress_buffer_.append(data, data_len);
- return true;
- });
+ 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;
- if (!decompress_ok) {
- body_reader_.last_error = Error::Read;
- return -1;
- }
+ Response res;
+ res.version = "HTTP/1.1";
+ res.headers = default_headers_;
- if (!decompress_buffer_.empty()) { break; }
+#ifdef __APPLE__
+ // Socket file descriptor exceeded FD_SETSIZE...
+ if (strm.socket() >= FD_SETSIZE) {
+ Headers dummy;
+ detail::read_headers(strm, dummy);
+ res.status = StatusCode::InternalServerError_500;
+ output_error_log(Error::ExceedMaxSocketDescriptorCount, &req);
+ return write_response(strm, close_connection, req, res);
}
+#endif
- auto to_copy = (std::min)(len, decompress_buffer_.size());
- std::memcpy(buf, decompress_buffer_.data(), to_copy);
- decompress_offset_ = to_copy;
- return static_cast<ssize_t>(to_copy);
-}
-
-void ClientImpl::StreamHandle::parse_trailers_if_needed() {
- if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) {
- return;
+ // 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);
}
- trailers_parsed_ = true;
+ // 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);
+ }
- const auto bufsiz = 128;
- char line_buf[bufsiz];
- detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz);
+ // Check if the request URI doesn't exceed the limit
+ if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
+ res.status = StatusCode::UriTooLong_414;
+ output_error_log(Error::ExceedUriMaxLength, &req);
+ return write_response(strm, close_connection, req, res);
+ }
- if (!line_reader.getline()) { return; }
+ if (req.get_header_value("Connection") == "close") {
+ connection_closed = true;
+ }
- if (!detail::parse_trailers(line_reader, response->trailers,
- response->headers)) {
- return;
+ if (req.version == "HTTP/1.0" &&
+ req.get_header_value("Connection") != "Keep-Alive") {
+ connection_closed = true;
}
-}
-// Inline method implementations for `ChunkedDecoder`.
-namespace detail {
+ if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) {
+ auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
+ req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_);
+ } else {
+ req.remote_addr = remote_addr;
+ }
+ req.remote_port = remote_port;
-ChunkedDecoder::ChunkedDecoder(Stream &s) : strm(s) {}
+ req.local_addr = local_addr;
+ req.local_port = local_port;
-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 (req.has_header("Accept")) {
+ const auto &accept_header = req.get_header_value("Accept");
+ if (!detail::parse_accept_header(accept_header, req.accept_content_types)) {
+ res.status = StatusCode::BadRequest_400;
+ output_error_log(Error::HTTPParsing, &req);
+ return write_response(strm, close_connection, req, res);
+ }
+ }
- if (chunk_remaining == 0) {
- stream_line_reader lr(strm, line_buf, sizeof(line_buf));
- if (!lr.getline()) { return -1; }
+ 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)) {
+ res.status = StatusCode::RangeNotSatisfiable_416;
+ output_error_log(Error::InvalidRangeHeader, &req);
+ return write_response(strm, close_connection, req, res);
+ }
+ }
- char *endptr = nullptr;
- unsigned long chunk_len = std::strtoul(lr.ptr(), &endptr, 16);
- if (endptr == lr.ptr()) { return -1; }
- if (chunk_len == ULONG_MAX) { return -1; }
+ if (setup_request) { setup_request(req); }
- if (chunk_len == 0) {
- chunk_remaining = 0;
- finished = true;
- out_chunk_offset = 0;
- out_chunk_total = 0;
- return 0;
+ 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);
}
-
- chunk_remaining = static_cast<size_t>(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; }
+ // Setup `is_connection_closed` method
+ auto sock = strm.socket();
+ req.is_connection_closed = [sock]() {
+ return !detail::is_socket_alive(sock);
+ };
- auto offset_before = last_chunk_offset;
- last_chunk_offset += static_cast<size_t>(n);
- chunk_remaining -= static_cast<size_t>(n);
+ // Routing
+ auto routed = false;
+#ifdef CPPHTTPLIB_NO_EXCEPTIONS
+ routed = routing(req, res, strm);
+#else
+ try {
+ routed = routing(req, res, strm);
+ } catch (std::exception &e) {
+ if (exception_handler_) {
+ auto ep = std::current_exception();
+ exception_handler_(req, res, ep);
+ routed = true;
+ } else {
+ res.status = StatusCode::InternalServerError_500;
+ std::string val;
+ auto s = e.what();
+ for (size_t i = 0; s[i]; i++) {
+ switch (s[i]) {
+ case '\r': val += "\\r"; break;
+ case '\n': val += "\\n"; break;
+ default: val += s[i]; break;
+ }
+ }
+ res.set_header("EXCEPTION_WHAT", val);
+ }
+ } catch (...) {
+ if (exception_handler_) {
+ auto ep = std::current_exception();
+ exception_handler_(req, res, ep);
+ routed = true;
+ } else {
+ res.status = StatusCode::InternalServerError_500;
+ res.set_header("EXCEPTION_WHAT", "UNKNOWN");
+ }
+ }
+#endif
+ if (routed) {
+ if (res.status == -1) {
+ res.status = req.ranges.empty() ? StatusCode::OK_200
+ : StatusCode::PartialContent_206;
+ }
- out_chunk_offset = offset_before;
- out_chunk_total = last_chunk_total;
+ // Serve file content by using a content provider
+ if (!res.file_content_path_.empty()) {
+ const auto &path = res.file_content_path_;
+ auto mm = std::make_shared<detail::mmap>(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);
+ return write_response(strm, close_connection, req, res);
+ }
- 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; }
- }
+ 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_);
+ }
- return n;
-}
+ 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;
+ });
+ }
-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);
-}
+ if (detail::range_error(req, res)) {
+ res.body.clear();
+ res.content_length_ = 0;
+ res.content_provider_ = nullptr;
+ res.status = StatusCode::RangeNotSatisfiable_416;
+ return write_response(strm, close_connection, req, res);
+ }
-} // namespace detail
+ return write_response_with_content(strm, close_connection, req, res);
+ } else {
+ if (res.status == -1) { res.status = StatusCode::NotFound_404; }
-void
-ClientImpl::transfer_socket_ownership_to_handle(StreamHandle &handle) {
- handle.connection_->sock = socket_.sock;
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- handle.connection_->ssl = socket_.ssl;
- socket_.ssl = nullptr;
-#endif
- socket_.sock = INVALID_SOCKET;
+ return write_response(strm, close_connection, req, res);
+ }
}
-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;
- }
+bool Server::is_valid() const { return true; }
- auto req_save = req;
+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);
- bool ret;
+ std::string local_addr;
+ int local_port = 0;
+ detail::get_local_ip_and_port(sock, local_addr, local_port);
- if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) {
- 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);
- }
+ 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);
+ });
- if (!ret) { return false; }
+ detail::shutdown_socket(sock);
+ detail::close_socket(sock);
+ return ret;
+}
- if (res.get_header_value("Connection") == "close" ||
- (res.version == "HTTP/1.0" && res.reason != "Connection established")) {
- // TODO this requires a not-entirely-obvious chain of calls to be correct
- // for this to be safe.
+void Server::output_log(const Request &req, const Response &res) const {
+ if (logger_) {
+ std::lock_guard<std::mutex> guard(logger_mutex_);
+ logger_(req, res);
+ }
+}
- // 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<std::mutex> guard(socket_mutex_);
- shutdown_ssl(socket_, true);
- shutdown_socket(socket_);
- close_socket(socket_);
+void Server::output_pre_compression_log(const Request &req,
+ const Response &res) const {
+ if (pre_compression_logger_) {
+ std::lock_guard<std::mutex> guard(logger_mutex_);
+ pre_compression_logger_(req, res);
}
+}
- if (300 < res.status && res.status < 400 && follow_location_) {
- req = std::move(req_save);
- ret = redirect(req, res, error);
+void Server::output_error_log(const Error &err,
+ const Request *req) const {
+ if (error_logger_) {
+ std::lock_guard<std::mutex> guard(logger_mutex_);
+ error_logger_(err, req);
}
+}
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- if ((res.status == StatusCode::Unauthorized_401 ||
- res.status == StatusCode::ProxyAuthenticationRequired_407) &&
- req.authorization_count_ < 5) {
- auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407;
- const auto &username =
- is_proxy ? proxy_digest_auth_username_ : digest_auth_username_;
- const auto &password =
- is_proxy ? proxy_digest_auth_password_ : digest_auth_password_;
+/*
+ * Group 5: ClientImpl and Client (Universal) implementation
+ */
+// HTTP client implementation
+ClientImpl::ClientImpl(const std::string &host)
+ : ClientImpl(host, 80, std::string(), std::string()) {}
- if (!username.empty() && !password.empty()) {
- std::map<std::string, std::string> 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));
+ClientImpl::ClientImpl(const std::string &host, int port)
+ : ClientImpl(host, port, std::string(), std::string()) {}
- Response new_res;
+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) {}
- ret = send(new_req, new_res, error);
- if (ret) { res = std::move(new_res); }
- }
+ClientImpl::~ClientImpl() {
+ // Wait until all the requests in flight are handled.
+ size_t retry_count = 10;
+ while (retry_count-- > 0) {
+ {
+ std::lock_guard<std::mutex> guard(socket_mutex_);
+ if (socket_requests_in_flight_ == 0) { break; }
}
+ std::this_thread::sleep_for(std::chrono::milliseconds{1});
}
-#endif
- return ret;
+ std::lock_guard<std::mutex> guard(socket_mutex_);
+ shutdown_socket(socket_);
+ close_socket(socket_);
}
-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; }
-
- thread_local const std::regex re(
- R"((?:(https?):)?(?://(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)");
-
- std::smatch m;
- if (!std::regex_match(location, m, re)) { return false; }
+bool ClientImpl::is_valid() const { return true; }
- auto scheme = is_ssl() ? "https" : "http";
+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_;
+ logger_ = rhs.logger_;
+ error_logger_ = rhs.error_logger_;
- auto next_scheme = m[1].str();
- auto next_host = m[2].str();
- if (next_host.empty()) { next_host = m[3].str(); }
- auto port_str = m[4].str();
- auto next_path = m[5].str();
- auto next_query = m[6].str();
+#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_;
+#endif
+}
- auto next_port = port_;
- if (!port_str.empty()) {
- next_port = std::stoi(port_str);
- } else if (!next_scheme.empty()) {
- next_port = next_scheme == "https" ? 443 : 80;
+socket_t ClientImpl::create_client_socket(Error &error) const {
+ if (!proxy_host_.empty() && proxy_port_ != -1) {
+ 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);
}
- if (next_scheme.empty()) { next_scheme = scheme; }
- if (next_host.empty()) { next_host = host_; }
- if (next_path.empty()) { next_path = "/"; }
+ // Check is custom IP specified for host_
+ std::string ip;
+ auto it = addr_map_.find(host_);
+ if (it != addr_map_.end()) { ip = it->second; }
- auto path = decode_query_component(next_path, true) + next_query;
+ 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);
+}
- // 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);
- }
+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;
+}
- // 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);
+bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
+ return create_and_connect_socket(socket, 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");
+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());
+}
- // 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<std::string>{"Host", "Proxy-Authorization", "Authorization"};
+void ClientImpl::shutdown_socket(Socket &socket) const {
+ if (socket.sock == INVALID_SOCKET) { return; }
+ detail::shutdown_socket(socket.sock);
+}
- 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);
- }
- }
+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());
- // Create appropriate client type and handle redirect
- if (need_ssl) {
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- // Create SSL client for HTTPS redirect
- SSLClient redirect_client(host, port);
+ // It is also a bug if this happens while SSL is still active
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ assert(socket.ssl == nullptr);
+#endif
- // Setup basic client configuration first
- setup_redirect_client(redirect_client);
+ if (socket.sock == INVALID_SOCKET) { return; }
+ detail::close_socket(socket.sock);
+ socket.sock = INVALID_SOCKET;
+}
- // SSL-specific configuration for proxy environments
- if (!proxy_host_.empty() && proxy_port_ != -1) {
- // Critical: Disable SSL verification for proxy environments
- redirect_client.enable_server_certificate_verification(false);
- redirect_client.enable_server_hostname_verification(false);
- } else {
- // For direct SSL connections, copy SSL verification settings
- redirect_client.enable_server_certificate_verification(
- server_certificate_verification_);
- redirect_client.enable_server_hostname_verification(
- server_hostname_verification_);
- }
+bool ClientImpl::read_response_line(Stream &strm, const Request &req,
+ Response &res,
+ bool skip_100_continue) const {
+ std::array<char, 2048> buf{};
- // Handle CA certificate store and paths if available
- if (ca_cert_store_ && X509_STORE_up_ref(ca_cert_store_)) {
- redirect_client.set_ca_cert_store(ca_cert_store_);
- }
- if (!ca_cert_file_path_.empty()) {
- redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_);
- }
+ detail::stream_line_reader line_reader(strm, buf.data(), buf.size());
- // 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
+ if (!line_reader.getline()) { return false; }
- // Execute the redirect
- return detail::redirect(redirect_client, req, res, path, location, error);
+#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+ thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n");
#else
- // SSL not supported - set appropriate error
- error = Error::SSLConnection;
- output_error_log(error, &req);
- return false;
+ thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n");
#endif
- } else {
- // HTTP redirect
- ClientImpl redirect_client(host, port);
- // Setup client with robust configuration
- setup_redirect_client(redirect_client);
+ 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]);
- // Execute the redirect
- return detail::redirect(redirect_client, req, res, path, location, error);
+ // 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;
}
-// New method for robust client setup (based on basic_manual_redirect.cpp
-// logic)
-template <typename ClientType>
-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_);
-
- // Copy authentication settings BEFORE proxy setup
- if (!basic_auth_username_.empty()) {
- client.set_basic_auth(basic_auth_username_, basic_auth_password_);
- }
- if (!bearer_token_auth_token_.empty()) {
- client.set_bearer_token_auth(bearer_token_auth_token_);
- }
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- if (!digest_auth_username_.empty()) {
- client.set_digest_auth(digest_auth_username_, digest_auth_password_);
+bool ClientImpl::send(Request &req, Response &res, Error &error) {
+ std::lock_guard<std::recursive_mutex> 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; }
}
-#endif
+ return ret;
+}
- // Setup proxy configuration (CRITICAL ORDER - proxy must be set
- // before proxy auth)
- if (!proxy_host_.empty() && proxy_port_ != -1) {
- // First set proxy host and port
- client.set_proxy(proxy_host_, proxy_port_);
+bool ClientImpl::send_(Request &req, Response &res, Error &error) {
+ {
+ std::lock_guard<std::mutex> guard(socket_mutex_);
- // Then set proxy authentication (order matters!)
- 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_OPENSSL_SUPPORT
- if (!proxy_digest_auth_username_.empty()) {
- client.set_proxy_digest_auth(proxy_digest_auth_username_,
- proxy_digest_auth_password_);
- }
-#endif
- }
+ // 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;
- // 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_); }
+ auto is_alive = false;
+ if (socket_.is_open()) {
+ is_alive = detail::is_socket_alive(socket_.sock);
- // Copy logging and headers
- if (logger_) { client.set_logger(logger_); }
- if (error_logger_) { client.set_error_logger(error_logger_); }
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ if (is_alive && is_ssl()) {
+ if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
+ is_alive = false;
+ }
+ }
+#endif
- // 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
-}
+ if (!is_alive) {
+ // Attempt to avoid sigpipe by shutting down non-gracefully if it
+ // seems like the other side has already closed the connection Also,
+ // there cannot be any requests in flight from other threads since we
+ // locked request_mutex_, so safe to close everything immediately
+ const bool shutdown_gracefully = false;
+ shutdown_ssl(socket_, shutdown_gracefully);
+ shutdown_socket(socket_);
+ close_socket(socket_);
+ }
+ }
-bool ClientImpl::write_content_with_provider(Stream &strm,
- const Request &req,
- Error &error) const {
- auto is_shutting_down = []() { return false; };
+ if (!is_alive) {
+ if (!ensure_socket_connection(socket_, error)) {
+ output_error_log(error, &req);
+ return false;
+ }
- if (req.is_chunked_content_provider_) {
- // TODO: Brotli support
- std::unique_ptr<detail::compressor> compressor;
-#ifdef CPPHTTPLIB_ZLIB_SUPPORT
- if (compress_) {
- compressor = detail::make_unique<detail::gzip_compressor>();
- } else
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ // TODO: refactoring
+ if (is_ssl()) {
+ auto &scli = static_cast<SSLClient &>(*this);
+ if (!proxy_host_.empty() && proxy_port_ != -1) {
+ auto success = false;
+ if (!scli.connect_with_proxy(socket_, req.start_time_, res, success,
+ error)) {
+ if (!success) { output_error_log(error, &req); }
+ return success;
+ }
+ }
+
+ if (!proxy_host_.empty() && proxy_port_ != -1) {
+ if (!scli.initialize_ssl(socket_, error)) {
+ output_error_log(error, &req);
+ return false;
+ }
+ }
+ }
#endif
- {
- compressor = detail::make_unique<detail::nocompressor>();
}
- 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);
+ // 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();
}
-}
-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");
+ for (const auto &header : default_headers_) {
+ if (req.headers.find(header.first) == req.headers.end()) {
+ req.headers.insert(header);
}
}
- 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);
+ auto ret = false;
+ auto close_connection = !keep_alive_;
- 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");
- }
+ auto se = detail::scope_exit([&]() {
+ // Briefly lock mutex in order to mark that a request is no longer ongoing
+ std::lock_guard<std::mutex> 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 (!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 (socket_should_be_closed_when_request_is_done_ || close_connection ||
+ !ret) {
+ shutdown_ssl(socket_, true);
+ shutdown_socket(socket_);
+ close_socket(socket_);
}
- }
+ });
- if (!proxy_basic_auth_username_.empty() &&
- !proxy_basic_auth_password_.empty()) {
- if (!req.has_header("Proxy-Authorization")) {
- req.headers.insert(make_basic_authentication_header(
- proxy_basic_auth_username_, proxy_basic_auth_password_, true));
- }
- }
+ ret = process_socket(socket_, req.start_time_, [&](Stream &strm) {
+ return handle_request(strm, req, res, close_connection, error);
+ });
- if (!bearer_token_auth_token_.empty()) {
- if (!req.has_header("Authorization")) {
- req.headers.insert(make_bearer_token_authentication_header(
- bearer_token_auth_token_, false));
+ if (!ret) {
+ if (error == Error::Success) {
+ error = Error::Unknown;
+ output_error_log(error, &req);
}
}
- if (!proxy_bearer_token_auth_token_.empty()) {
- if (!req.has_header("Proxy-Authorization")) {
- req.headers.insert(make_bearer_token_authentication_header(
- proxy_bearer_token_auth_token_, true));
- }
- }
+ return ret;
+}
- // Request line and headers
- {
- detail::BufferStream bstrm;
+Result ClientImpl::send(const Request &req) {
+ auto req2 = req;
+ return send_(std::move(req2));
+}
- // 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);
+Result ClientImpl::send_(Request &&req) {
+ auto res = detail::make_unique<Response>();
+ 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 {
- path_part = req.path;
- query_part = "";
+ r.headers.emplace(
+ "Host", detail::make_host_and_port_string(host_, port_, is_ssl()));
}
+ }
- // 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 (!r.has_header("Accept")) { r.headers.emplace("Accept", "*/*"); }
- 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);
- }
+ 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);
}
- // 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;
+#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
+ }
- // 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;
+ 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()));
}
}
+}
- // 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();
+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<Response>();
+ handle.error = Error::Success;
- 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;
- }
+ auto query_path = params.empty() ? path : append_query_params(path, params);
+ handle.connection_ = detail::make_unique<ClientConnection>();
- // 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; }
+ {
+ std::lock_guard<std::mutex> guard(socket_mutex_);
- auto now = std::chrono::high_resolution_clock::now();
- auto elapsed =
- std::chrono::duration_cast<std::chrono::milliseconds>(now - start)
- .count();
- if (elapsed >= CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND) {
- break;
+ 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) {
+ shutdown_ssl(socket_, false);
+ shutdown_socket(socket_);
+ close_socket(socket_);
}
+ }
- std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ if (!is_alive) {
+ if (!ensure_socket_connection(socket_, handle.error)) {
+ handle.response.reset();
+ return handle;
+ }
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ if (is_ssl()) {
+ auto &scli = static_cast<SSLClient &>(*this);
+ if (!proxy_host_.empty() && proxy_port_ != -1) {
+ if (!scli.initialize_ssl(socket_, handle.error)) {
+ handle.response.reset();
+ return handle;
+ }
+ }
+ }
+#endif
}
+
+ transfer_socket_ownership_to_handle(handle);
+ }
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ if (is_ssl() && handle.connection_->session) {
+ handle.socket_stream_ = detail::make_unique<detail::SSLSocketStream>(
+ 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<detail::SocketStream>(
+ handle.connection_->sock, read_timeout_sec_, read_timeout_usec_,
+ write_timeout_sec_, write_timeout_usec_);
}
+#else
+ handle.socket_stream_ = detail::make_unique<detail::SocketStream>(
+ handle.connection_->sock, read_timeout_sec_, read_timeout_usec_,
+ write_timeout_sec_, write_timeout_usec_);
#endif
+ handle.stream_ = handle.socket_stream_.get();
- // Body
- if (skip_body) { return true; }
+ Request req;
+ req.method = method;
+ req.path = query_path;
+ req.headers = headers;
+ req.body = body;
- return write_request_body(strm, req, error);
-}
+ prepare_default_headers(req, true, content_type);
-bool ClientImpl::write_request_body(Stream &strm, Request &req,
- Error &error) {
- if (req.body.empty()) {
- return write_content_with_provider(strm, req, error);
+ 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 (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 (!detail::check_and_write_headers(strm, req.headers, header_writer_,
+ handle.error)) {
+ handle.response.reset();
+ return handle;
+ }
- 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;
+ if (!body.empty()) {
+ if (strm.write(body.data(), body.size()) < 0) {
+ handle.error = Error::Write;
+ handle.response.reset();
+ return handle;
}
}
- return true;
-}
+ if (!read_response_line(strm, req, *handle.response) ||
+ !detail::read_headers(strm, handle.response->headers)) {
+ handle.error = Error::Read;
+ handle.response.reset();
+ return handle;
+ }
-std::unique_ptr<Response>
-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); }
+ handle.body_reader_.stream = handle.stream_;
+ handle.body_reader_.payload_max_length = payload_max_length_;
-#ifdef CPPHTTPLIB_ZLIB_SUPPORT
- if (compress_) { req.set_header("Content-Encoding", "gzip"); }
-#endif
+ auto content_length_str = handle.response->get_header_value("Content-Length");
+ if (!content_length_str.empty()) {
+ handle.body_reader_.has_content_length = true;
+ handle.body_reader_.content_length =
+ static_cast<size_t>(std::stoull(content_length_str));
+ }
-#ifdef CPPHTTPLIB_ZLIB_SUPPORT
- if (compress_ && !content_provider_without_length) {
- // TODO: Brotli support
- detail::gzip_compressor compressor;
+ auto transfer_encoding =
+ handle.response->get_header_value("Transfer-Encoding");
+ handle.body_reader_.chunked = (transfer_encoding == "chunked");
- if (content_provider) {
- auto ok = true;
- size_t offset = 0;
- DataSink data_sink;
+ auto content_encoding = handle.response->get_header_value("Content-Encoding");
+ if (!content_encoding.empty()) {
+ handle.decompressor_ = detail::create_decompressor(content_encoding);
+ }
- data_sink.write = [&](const char *data, size_t data_len) -> bool {
- if (ok) {
- auto last = offset + data_len == content_length;
+ return handle;
+}
- 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;
- });
+ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) {
+ if (!is_valid() || !response) { return -1; }
- if (ret) {
- offset += data_len;
- } else {
- ok = false;
- }
- }
- return ok;
- };
+ if (decompressor_) { return read_with_decompression(buf, len); }
+ auto n = detail::read_body_content(stream_, body_reader_, buf, len);
- while (ok && offset < content_length) {
- if (!content_provider(offset, content_length - offset, data_sink)) {
- error = Error::Canceled;
- output_error_log(error, &req);
- return nullptr;
- }
+ 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 {
- 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;
+ detail::ChunkedDecoder dec(*stream_);
+ if (!dec.parse_trailers_into(response->trailers, response->headers)) {
+ return n;
}
}
- } else
-#endif
- {
- 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<Response>();
- return send(req, *res, error) ? std::move(res) : nullptr;
+ return n;
}
-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();
+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<ssize_t>(to_copy);
}
- auto error = Error::Success;
+ decompress_buffer_.clear();
+ decompress_offset_ = 0;
- 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);
+ constexpr size_t kDecompressionBufferSize = 8192;
+ char compressed_buf[kDecompressionBufferSize];
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- return Result{std::move(res), error, std::move(req.headers), last_ssl_error_,
- last_openssl_error_};
-#else
- return Result{std::move(res), error, std::move(req.headers)};
-#endif
-}
+ while (true) {
+ auto n = detail::read_body_content(stream_, body_reader_, compressed_buf,
+ sizeof(compressed_buf));
-void ClientImpl::output_log(const Request &req,
- const Response &res) const {
- if (logger_) {
- std::lock_guard<std::mutex> guard(logger_mutex_);
- logger_(req, res);
+ if (n <= 0) { return n; }
+
+ bool decompress_ok = decompressor_->decompress(
+ compressed_buf, static_cast<size_t>(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<ssize_t>(to_copy);
}
-void ClientImpl::output_error_log(const Error &err,
- const Request *req) const {
- if (error_logger_) {
- std::lock_guard<std::mutex> guard(logger_mutex_);
- error_logger_(err, req);
+void ClientImpl::StreamHandle::parse_trailers_if_needed() {
+ if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) {
+ return;
}
-}
-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");
- }
+ 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;
}
+}
- // Check for Expect: 100-continue
- auto expect_100_continue = req.get_header_value("Expect") == "100-continue";
+namespace detail {
- // Send request (skip body if using Expect: 100-continue)
- auto write_request_success =
- write_request(strm, req, close_connection, error, expect_100_continue);
+ChunkedDecoder::ChunkedDecoder(Stream &s) : strm(s) {}
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- if (is_ssl()) {
- auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1;
- if (!is_proxy_enabled) {
- if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) {
- error = Error::SSLPeerCouldBeClosed_;
- output_error_log(error, &req);
- return false;
- }
+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; }
+
+ char *endptr = nullptr;
+ unsigned long chunk_len = std::strtoul(lr.ptr(), &endptr, 16);
+ if (endptr == lr.ptr()) { return -1; }
+ if (chunk_len == ULONG_MAX) { return -1; }
+
+ if (chunk_len == 0) {
+ chunk_remaining = 0;
+ finished = true;
+ out_chunk_offset = 0;
+ out_chunk_total = 0;
+ return 0;
}
+
+ chunk_remaining = static_cast<size_t>(chunk_len);
+ last_chunk_total = chunk_remaining;
+ last_chunk_offset = 0;
}
-#endif
- // Handle Expect: 100-continue with timeout
- if (expect_100_continue && 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;
- auto ret = detail::select_read(strm.socket(), sec, usec);
- if (ret <= 0) {
- // Timeout or error: send body anyway (server didn't respond in time)
- if (!write_request_body(strm, req, error)) { return false; }
- expect_100_continue = false; // Switch to normal response handling
- }
+ 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<size_t>(n);
+ chunk_remaining -= static_cast<size_t>(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; }
}
- // Receive response and headers
- // When using Expect: 100-continue, don't auto-skip `100 Continue` response
- if (!read_response_line(strm, req, res, !expect_100_continue) ||
- !detail::read_headers(strm, res.headers)) {
- if (write_request_success) { error = Error::Read; }
+ 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;
}
- if (!write_request_success) { return false; }
+ auto req_save = req;
- // 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; }
+ bool ret;
- // 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
+ if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) {
+ 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);
}
- // 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 (!ret) { return false; }
- if (req.response_handler && !redirect) {
- if (!req.response_handler(res)) {
- error = Error::Canceled;
- output_error_log(error, &req);
- return false;
- }
- }
+ if (res.get_header_value("Connection") == "close" ||
+ (res.version == "HTTP/1.0" && res.reason != "Connection established")) {
+ // TODO this requires a not-entirely-obvious chain of calls to be correct
+ // for this to be safe.
- auto out =
- req.content_receiver
- ? static_cast<ContentReceiverWithProgress>(
- [&](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<ContentReceiverWithProgress>(
- [&](const char *buf, size_t n, size_t /*off*/,
- size_t /*len*/) {
- assert(res.body.size() + n <= res.body.max_size());
- res.body.append(buf, n);
- return true;
- });
+ // 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<std::mutex> guard(socket_mutex_);
+ shutdown_ssl(socket_, true);
+ shutdown_socket(socket_);
+ close_socket(socket_);
+ }
- 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 (300 < res.status && res.status < 400 && follow_location_) {
+ req = std::move(req_save);
+ ret = redirect(req, res, error);
+ }
- 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;
- }
- res.body.reserve(static_cast<size_t>(len));
- }
- }
+#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;
+ 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 (res.status != StatusCode::NotModified_304) {
- int dummy_status;
- if (!detail::read_content(strm, res, (std::numeric_limits<size_t>::max)(),
- dummy_status, std::move(progress),
- std::move(out), decompress_)) {
- if (error != Error::Canceled) { error = Error::Read; }
- output_error_log(error, &req);
- return false;
+ if (!username.empty() && !password.empty()) {
+ std::map<std::string, std::string> 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
- // Log
- output_log(req, res);
-
- return true;
+ return ret;
}
-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;
- }
+bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
+ if (req.redirect_count_ == 0) {
+ error = Error::ExceedRedirectCount;
+ output_error_log(error, &req);
+ return false;
+ }
- DataSink cur_sink;
- auto has_data = true;
- cur_sink.write = sink.write;
- cur_sink.done = [&]() { has_data = false; };
+ auto location = res.get_header_value("location");
+ if (location.empty()) { return false; }
- if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) {
- return false;
- }
+ thread_local const std::regex re(
+ R"((?:(https?):)?(?://(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)");
- 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;
- }
- };
-}
+ std::smatch m;
+ if (!std::regex_match(location, m, re)) { return false; }
-bool ClientImpl::process_socket(
- const Socket &socket,
- std::chrono::time_point<std::chrono::steady_clock> start_time,
- std::function<bool(Stream &strm)> 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));
-}
+ auto scheme = is_ssl() ? "https" : "http";
-bool ClientImpl::is_ssl() const { return false; }
+ auto next_scheme = m[1].str();
+ auto next_host = m[2].str();
+ if (next_host.empty()) { next_host = m[3].str(); }
+ auto port_str = m[4].str();
+ auto next_path = m[5].str();
+ auto next_query = m[6].str();
-Result ClientImpl::Get(const std::string &path,
- DownloadProgress progress) {
- return Get(path, Headers(), std::move(progress));
-}
+ auto next_port = port_;
+ if (!port_str.empty()) {
+ next_port = std::stoi(port_str);
+ } else if (!next_scheme.empty()) {
+ next_port = next_scheme == "https" ? 443 : 80;
+ }
-Result ClientImpl::Get(const std::string &path, const Params ¶ms,
- const Headers &headers,
- DownloadProgress progress) {
- if (params.empty()) { return Get(path, headers); }
+ if (next_scheme.empty()) { next_scheme = scheme; }
+ if (next_host.empty()) { next_host = host_; }
+ if (next_path.empty()) { next_path = "/"; }
- std::string path_with_query = append_query_params(path, params);
- return Get(path_with_query, headers, std::move(progress));
-}
+ auto path = decode_query_component(next_path, true) + next_query;
-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();
+ // 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);
}
- return send_(std::move(req));
+ // 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);
}
-Result ClientImpl::Get(const std::string &path,
- ContentReceiver content_receiver,
- DownloadProgress progress) {
- return Get(path, Headers(), nullptr, std::move(content_receiver),
- std::move(progress));
-}
+// 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");
-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));
-}
+ // 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<std::string>{"Host", "Proxy-Authorization", "Authorization"};
-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();
+ 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);
+ }
}
- return send_(std::move(req));
-}
+ // 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);
-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));
-}
+ // Setup basic client configuration first
+ setup_redirect_client(redirect_client);
-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));
- }
+ // SSL-specific configuration for proxy environments
+ if (!proxy_host_.empty() && proxy_port_ != -1) {
+ // Critical: Disable SSL verification for proxy environments
+ redirect_client.enable_server_certificate_verification(false);
+ redirect_client.enable_server_hostname_verification(false);
+ } else {
+ // For direct SSL connections, copy SSL verification settings
+ redirect_client.enable_server_certificate_verification(
+ server_certificate_verification_);
+ redirect_client.enable_server_hostname_verification(
+ server_hostname_verification_);
+ }
- 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));
-}
+ // 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_);
+ }
-Result ClientImpl::Head(const std::string &path) {
- return Head(path, Headers());
-}
+ // 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
-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();
- }
+ // 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);
- return send_(std::move(req));
-}
+ // Setup client with robust configuration
+ setup_redirect_client(redirect_client);
-Result ClientImpl::Post(const std::string &path) {
- return Post(path, std::string(), std::string());
+ // Execute the redirect
+ return detail::redirect(redirect_client, req, res, path, location, error);
+ }
}
-Result ClientImpl::Post(const std::string &path,
- const Headers &headers) {
- return Post(path, headers, nullptr, 0, std::string());
-}
+// New method for robust client setup (based on basic_manual_redirect.cpp
+// logic)
+template <typename ClientType>
+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_);
-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);
-}
+ // Copy authentication settings BEFORE proxy setup
+ if (!basic_auth_username_.empty()) {
+ client.set_basic_auth(basic_auth_username_, basic_auth_password_);
+ }
+ if (!bearer_token_auth_token_.empty()) {
+ client.set_bearer_token_auth(bearer_token_auth_token_);
+ }
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ if (!digest_auth_username_.empty()) {
+ client.set_digest_auth(digest_auth_username_, digest_auth_password_);
+ }
+#endif
-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);
-}
+ // Setup proxy configuration (CRITICAL ORDER - proxy must be set
+ // before proxy auth)
+ if (!proxy_host_.empty() && proxy_port_ != -1) {
+ // First set proxy host and port
+ client.set_proxy(proxy_host_, proxy_port_);
-Result ClientImpl::Post(const std::string &path, const Params ¶ms) {
- return Post(path, Headers(), params);
-}
+ // Then set proxy authentication (order matters!)
+ 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
+ }
-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);
-}
+ // 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_); }
-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);
-}
+ // Copy logging and headers
+ if (logger_) { client.set_logger(logger_); }
+ if (error_logger_) { client.set_error_logger(error_logger_); }
-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);
+ // 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
}
-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);
-}
+bool ClientImpl::write_content_with_provider(Stream &strm,
+ const Request &req,
+ Error &error) const {
+ auto is_shutting_down = []() { return false; };
-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");
-}
+ if (req.is_chunked_content_provider_) {
+ // TODO: Brotli support
+ std::unique_ptr<detail::compressor> compressor;
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+ if (compress_) {
+ compressor = detail::make_unique<detail::gzip_compressor>();
+ } else
+#endif
+ {
+ compressor = detail::make_unique<detail::nocompressor>();
+ }
-Result ClientImpl::Post(const std::string &path,
- const UploadFormDataItems &items,
- UploadProgress progress) {
- return Post(path, Headers(), items, progress);
+ 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);
+ }
}
-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);
- const auto &body = detail::serialize_multipart_formdata(items, boundary);
- return Post(path, headers, body, content_type, progress);
-}
+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");
+ }
+ }
-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};
+ 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);
- const auto &content_type =
- detail::serialize_multipart_formdata_get_content_type(boundary);
- const auto &body = detail::serialize_multipart_formdata(items, boundary);
- return Post(path, headers, body, content_type, progress);
-}
+ 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");
+ }
+ }
+ }
-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);
-}
+ 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));
+ }
+ }
-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);
-}
+ if (!proxy_basic_auth_username_.empty() &&
+ !proxy_basic_auth_password_.empty()) {
+ if (!req.has_header("Proxy-Authorization")) {
+ req.headers.insert(make_basic_authentication_header(
+ proxy_basic_auth_username_, proxy_basic_auth_password_, true));
+ }
+ }
-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);
-}
+ if (!bearer_token_auth_token_.empty()) {
+ if (!req.has_header("Authorization")) {
+ req.headers.insert(make_bearer_token_authentication_header(
+ bearer_token_auth_token_, false));
+ }
+ }
-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));
-}
+ if (!proxy_bearer_token_auth_token_.empty()) {
+ if (!req.has_header("Proxy-Authorization")) {
+ req.headers.insert(make_bearer_token_authentication_header(
+ proxy_bearer_token_auth_token_, true));
+ }
+ }
-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);
-}
+ // Request line and headers
+ {
+ detail::BufferStream bstrm;
-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));
-}
+ // 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 = "";
+ }
-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);
-}
+ // 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;
-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 (!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; }
- if (max_timeout_msec_ > 0) {
- req.start_time_ = std::chrono::steady_clock::now();
+ // 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;
+ }
}
- if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
+ // 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();
- return send_(std::move(req));
-}
+ 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;
+ }
-Result ClientImpl::Put(const std::string &path) {
- return Put(path, std::string(), std::string());
-}
+ // 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; }
-Result ClientImpl::Put(const std::string &path, const Headers &headers) {
- return Put(path, headers, nullptr, 0, std::string());
-}
+ auto now = std::chrono::high_resolution_clock::now();
+ auto elapsed =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - start)
+ .count();
+ if (elapsed >= CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND) {
+ break;
+ }
-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);
-}
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+ }
+#endif
-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);
-}
+ // Body
+ if (skip_body) { return true; }
-Result ClientImpl::Put(const std::string &path, const Params ¶ms) {
- return Put(path, Headers(), params);
+ return write_request_body(strm, req, error);
}
-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");
-}
+bool ClientImpl::write_request_body(Stream &strm, Request &req,
+ Error &error) {
+ if (req.body.empty()) {
+ return write_content_with_provider(strm, req, error);
+ }
-Result ClientImpl::Put(const std::string &path,
- const UploadFormDataItems &items,
- UploadProgress progress) {
- return Put(path, Headers(), items, progress);
-}
+ if (req.upload_progress) {
+ auto body_size = req.body.size();
+ size_t written = 0;
+ auto data = req.body.data();
-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);
- const auto &body = detail::serialize_multipart_formdata(items, boundary);
- return Put(path, headers, body, content_type, progress);
-}
+ 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;
-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};
+ 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;
+ }
}
- const auto &content_type =
- detail::serialize_multipart_formdata_get_content_type(boundary);
- const auto &body = detail::serialize_multipart_formdata(items, boundary);
- return Put(path, headers, body, 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);
+ return true;
}
-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);
-}
+std::unique_ptr<Response>
+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); }
-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);
-}
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+ if (compress_) { req.set_header("Content-Encoding", "gzip"); }
+#endif
-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);
-}
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+ if (compress_ && !content_provider_without_length) {
+ // TODO: Brotli support
+ detail::gzip_compressor compressor;
-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);
-}
+ if (content_provider) {
+ auto ok = true;
+ size_t offset = 0;
+ DataSink data_sink;
-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);
-}
+ data_sink.write = [&](const char *data, size_t data_len) -> bool {
+ if (ok) {
+ auto last = offset + data_len == content_length;
-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);
-}
+ 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;
+ });
-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);
+ if (ret) {
+ offset += data_len;
+ } else {
+ ok = false;
+ }
+ }
+ return ok;
};
- req.download_progress = std::move(progress);
- if (max_timeout_msec_ > 0) {
- req.start_time_ = std::chrono::steady_clock::now();
+ 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
+#endif
+ {
+ 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_type.empty()) { req.set_header("Content-Type", content_type); }
+ 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);
+ };
+ }
- return send_(std::move(req));
+ auto res = detail::make_unique<Response>();
+ return send(req, *res, error) ? std::move(res) : nullptr;
}
-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::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();
+ }
-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);
-}
+ auto error = Error::Success;
-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);
-}
+ 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);
-Result ClientImpl::Patch(const std::string &path, const Params ¶ms) {
- return Patch(path, Headers(), params);
+#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
}
-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);
+void ClientImpl::output_log(const Request &req,
+ const Response &res) const {
+ if (logger_) {
+ std::lock_guard<std::mutex> guard(logger_mutex_);
+ logger_(req, res);
+ }
}
-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);
+void ClientImpl::output_error_log(const Error &err,
+ const Request *req) const {
+ if (error_logger_) {
+ std::lock_guard<std::mutex> guard(logger_mutex_);
+ error_logger_(err, req);
+ }
}
-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);
-}
+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");
+ }
+ }
-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);
-}
+ // Check for Expect: 100-continue
+ auto expect_100_continue = req.get_header_value("Expect") == "100-continue";
-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");
-}
+ // Send request (skip body if using Expect: 100-continue)
+ auto write_request_success =
+ write_request(strm, req, close_connection, error, expect_100_continue);
-Result ClientImpl::Patch(const std::string &path,
- const UploadFormDataItems &items,
- UploadProgress progress) {
- return Patch(path, Headers(), items, progress);
-}
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ if (is_ssl() && !expect_100_continue) {
+ auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1;
+ if (!is_proxy_enabled) {
+ if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
+ error = Error::SSLPeerCouldBeClosed_;
+ output_error_log(error, &req);
+ return false;
+ }
+ }
+ }
+#endif
-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);
- const auto &body = detail::serialize_multipart_formdata(items, boundary);
- return Patch(path, headers, body, content_type, progress);
-}
+ // Handle Expect: 100-continue with timeout
+ if (expect_100_continue && 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;
+ auto ret = detail::select_read(strm.socket(), sec, usec);
+ if (ret <= 0) {
+ // Timeout or error: send body anyway (server didn't respond in time)
+ if (!write_request_body(strm, req, error)) { return false; }
+ expect_100_continue = false; // Switch to normal response handling
+ }
+ }
-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};
+ // Receive response and headers
+ // When using Expect: 100-continue, don't auto-skip `100 Continue` response
+ if (!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;
}
- const auto &content_type =
- detail::serialize_multipart_formdata_get_content_type(boundary);
- const auto &body = detail::serialize_multipart_formdata(items, boundary);
- return Patch(path, headers, body, content_type, progress);
-}
+ if (!write_request_success) { return false; }
-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);
-}
+ // 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; }
-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);
-}
+ // 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
+ }
-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);
-}
+ // 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_;
-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);
-}
+ if (req.response_handler && !redirect) {
+ if (!req.response_handler(res)) {
+ error = Error::Canceled;
+ output_error_log(error, &req);
+ return false;
+ }
+ }
-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);
-}
+ auto out =
+ req.content_receiver
+ ? static_cast<ContentReceiverWithProgress>(
+ [&](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<ContentReceiverWithProgress>(
+ [&](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;
+ });
-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);
-}
+ 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;
+ };
-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 (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;
+ }
+ res.body.reserve(static_cast<size_t>(len));
+ }
+ }
- if (max_timeout_msec_ > 0) {
- req.start_time_ = std::chrono::steady_clock::now();
+ if (res.status != StatusCode::NotModified_304) {
+ int dummy_status;
+ auto max_length = (!has_payload_max_length_ && req.content_receiver)
+ ? (std::numeric_limits<size_t>::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;
+ }
+ }
}
- if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
+ // Log
+ output_log(req, res);
- return send_(std::move(req));
+ return true;
}
-Result ClientImpl::Delete(const std::string &path,
- DownloadProgress progress) {
- return Delete(path, Headers(), std::string(), std::string(), progress);
-}
+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;
+ }
-Result ClientImpl::Delete(const std::string &path,
- const Headers &headers,
- DownloadProgress progress) {
- return Delete(path, headers, std::string(), std::string(), progress);
-}
+ DataSink cur_sink;
+ auto has_data = true;
+ cur_sink.write = sink.write;
+ cur_sink.done = [&]() { has_data = false; };
-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);
-}
+ if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) {
+ return false;
+ }
-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);
+ 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;
+ }
+ };
}
-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);
+bool ClientImpl::process_socket(
+ const Socket &socket,
+ std::chrono::time_point<std::chrono::steady_clock> start_time,
+ std::function<bool(Stream &strm)> 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));
}
-Result ClientImpl::Delete(const std::string &path, const Params ¶ms,
- DownloadProgress progress) {
- return Delete(path, Headers(), params, progress);
+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::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::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::Delete(const std::string &path,
- const Headers &headers, const char *body,
- size_t content_length,
- const std::string &content_type,
- DownloadProgress progress) {
+Result ClientImpl::Get(const std::string &path, const Headers &headers,
+ DownloadProgress progress) {
Request req;
- req.method = "DELETE";
- req.headers = headers;
+ 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();
}
- 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::Get(const std::string &path,
+ ContentReceiver content_receiver,
+ DownloadProgress progress) {
+ return Get(path, Headers(), nullptr, std::move(content_receiver),
+ std::move(progress));
}
-Result ClientImpl::Options(const std::string &path,
- const Headers &headers) {
+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 = "OPTIONS";
- req.headers = headers;
+ 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));
}
-void ClientImpl::stop() {
- std::lock_guard<std::mutex> 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_);
+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));
+}
- // 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;
+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));
}
- // Otherwise, still holding the mutex, we can shut everything down ourselves
- shutdown_ssl(socket_, true);
- shutdown_socket(socket_);
- close_socket(socket_);
+ 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));
}
-std::string ClientImpl::host() const { return host_; }
+Result ClientImpl::Head(const std::string &path) {
+ return Head(path, Headers());
+}
-int ClientImpl::port() const { return port_; }
+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();
+ }
-size_t ClientImpl::is_socket_open() const {
- std::lock_guard<std::mutex> guard(socket_mutex_);
- return socket_.is_open();
+ return send_(std::move(req));
}
-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;
+Result ClientImpl::Post(const std::string &path) {
+ return Post(path, std::string(), std::string());
}
-void ClientImpl::set_read_timeout(time_t sec, time_t usec) {
- read_timeout_sec_ = sec;
- read_timeout_usec_ = usec;
+Result ClientImpl::Post(const std::string &path,
+ const Headers &headers) {
+ return Post(path, headers, nullptr, 0, std::string());
}
-void ClientImpl::set_write_timeout(time_t sec, time_t usec) {
- write_timeout_sec_ = sec;
- write_timeout_usec_ = usec;
+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);
}
-void ClientImpl::set_max_timeout(time_t msec) {
- max_timeout_msec_ = msec;
+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);
}
-void ClientImpl::set_basic_auth(const std::string &username,
- const std::string &password) {
- basic_auth_username_ = username;
- basic_auth_password_ = password;
+Result ClientImpl::Post(const std::string &path, const Params ¶ms) {
+ return Post(path, Headers(), params);
}
-void ClientImpl::set_bearer_token_auth(const std::string &token) {
- bearer_token_auth_token_ = token;
+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);
}
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-void ClientImpl::set_digest_auth(const std::string &username,
- const std::string &password) {
- digest_auth_username_ = username;
- digest_auth_password_ = password;
+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);
}
-#endif
-
-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<std::string, std::string> addr_map) {
- addr_map_ = std::move(addr_map);
+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);
}
-void ClientImpl::set_default_headers(Headers headers) {
- default_headers_ = std::move(headers);
+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);
}
-void ClientImpl::set_header_writer(
- std::function<ssize_t(Stream &, Headers &)> const &writer) {
- header_writer_ = writer;
+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");
}
-void ClientImpl::set_address_family(int family) {
- address_family_ = family;
+Result ClientImpl::Post(const std::string &path,
+ const UploadFormDataItems &items,
+ UploadProgress progress) {
+ return Post(path, Headers(), items, progress);
}
-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);
+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);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
+ return Post(path, headers, body, content_type, progress);
}
-void ClientImpl::set_compress(bool on) { compress_ = on; }
-
-void ClientImpl::set_decompress(bool on) { decompress_ = on; }
+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};
+ }
-void ClientImpl::set_interface(const std::string &intf) {
- interface_ = intf;
+ const auto &content_type =
+ detail::serialize_multipart_formdata_get_content_type(boundary);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
+ return Post(path, headers, body, content_type, progress);
}
-void ClientImpl::set_proxy(const std::string &host, int port) {
- proxy_host_ = host;
- proxy_port_ = port;
+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);
}
-void ClientImpl::set_proxy_basic_auth(const std::string &username,
- const std::string &password) {
- proxy_basic_auth_username_ = username;
- proxy_basic_auth_password_ = password;
+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);
}
-void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) {
- proxy_bearer_token_auth_token_ = token;
+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);
}
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-void ClientImpl::set_proxy_digest_auth(const std::string &username,
- const std::string &password) {
- proxy_digest_auth_username_ = username;
- proxy_digest_auth_password_ = password;
+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));
}
-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;
+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);
}
-void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) {
- if (ca_cert_store && ca_cert_store != ca_cert_store_) {
- ca_cert_store_ = ca_cert_store;
- }
+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));
}
-X509_STORE *ClientImpl::create_ca_cert_store(const char *ca_cert,
- std::size_t size) const {
- auto mem = BIO_new_mem_buf(ca_cert, static_cast<int>(size));
- auto se = detail::scope_exit([&] { BIO_free_all(mem); });
- if (!mem) { return nullptr; }
-
- auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr);
- if (!inf) { return nullptr; }
+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);
+}
- auto cts = X509_STORE_new();
- if (cts) {
- for (auto i = 0; i < static_cast<int>(sk_X509_INFO_num(inf)); i++) {
- auto itmp = sk_X509_INFO_value(inf, i);
- if (!itmp) { continue; }
+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 (itmp->x509) { X509_STORE_add_cert(cts, itmp->x509); }
- if (itmp->crl) { X509_STORE_add_crl(cts, itmp->crl); }
- }
+ if (max_timeout_msec_ > 0) {
+ req.start_time_ = std::chrono::steady_clock::now();
}
- sk_X509_INFO_pop_free(inf, X509_INFO_free);
- return cts;
-}
+ if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
-void ClientImpl::enable_server_certificate_verification(bool enabled) {
- server_certificate_verification_ = enabled;
+ return send_(std::move(req));
}
-void ClientImpl::enable_server_hostname_verification(bool enabled) {
- server_hostname_verification_ = enabled;
+Result ClientImpl::Put(const std::string &path) {
+ return Put(path, std::string(), std::string());
}
-void ClientImpl::set_server_certificate_verifier(
- std::function<SSLVerifierResponse(SSL *ssl)> verifier) {
- server_certificate_verifier_ = verifier;
+Result ClientImpl::Put(const std::string &path, const Headers &headers) {
+ return Put(path, headers, nullptr, 0, std::string());
}
-#endif
-void ClientImpl::set_logger(Logger logger) {
- logger_ = std::move(logger);
+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);
}
-void ClientImpl::set_error_logger(ErrorLogger error_logger) {
- error_logger_ = std::move(error_logger);
+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);
}
-/*
- * SSL Implementation
- */
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-namespace detail {
-
-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;
+Result ClientImpl::Put(const std::string &path, const Params ¶ms) {
+ return Put(path, Headers(), params);
}
-template <typename U, typename V>
-SSL *ssl_new(socket_t sock, SSL_CTX *ctx, std::mutex &ctx_mutex,
- U SSL_connect_or_accept, V setup) {
- SSL *ssl = nullptr;
- {
- std::lock_guard<std::mutex> guard(ctx_mutex);
- ssl = SSL_new(ctx);
- }
-
- if (ssl) {
- set_nonblocking(sock, true);
- auto bio = BIO_new_socket(static_cast<int>(sock), BIO_NOCLOSE);
- BIO_set_nbio(bio, 1);
- SSL_set_bio(ssl, bio, bio);
-
- if (!setup(ssl) || SSL_connect_or_accept(ssl) != 1) {
- SSL_shutdown(ssl);
- {
- std::lock_guard<std::mutex> guard(ctx_mutex);
- SSL_free(ssl);
- }
- set_nonblocking(sock, false);
- return nullptr;
- }
- BIO_set_nbio(bio, 0);
- set_nonblocking(sock, false);
- }
-
- return ssl;
+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);
}
-void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, socket_t sock,
- bool shutdown_gracefully) {
- // sometimes we may want to skip this to try to avoid SIGPIPE if we know
- // the remote has closed the network connection
- // Note that it is not always possible to avoid SIGPIPE, this is merely a
- // best-efforts.
- if (shutdown_gracefully) {
- (void)(sock);
- // SSL_shutdown() returns 0 on first call (indicating close_notify alert
- // sent) and 1 on subsequent call (indicating close_notify alert received)
- if (SSL_shutdown(ssl) == 0) {
- // Expected to return 1, but even if it doesn't, we free ssl
- SSL_shutdown(ssl);
- }
- }
-
- std::lock_guard<std::mutex> guard(ctx_mutex);
- SSL_free(ssl);
+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);
}
-template <typename U>
-bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl,
- U ssl_connect_or_accept,
- time_t timeout_sec, time_t timeout_usec,
- int *ssl_error) {
- auto res = 0;
- while ((res = ssl_connect_or_accept(ssl)) != 1) {
- auto err = SSL_get_error(ssl, res);
- switch (err) {
- case SSL_ERROR_WANT_READ:
- if (select_read(sock, timeout_sec, timeout_usec) > 0) { continue; }
- break;
- case SSL_ERROR_WANT_WRITE:
- if (select_write(sock, timeout_sec, timeout_usec) > 0) { continue; }
- break;
- default: break;
- }
- if (ssl_error) { *ssl_error = err; }
- return false;
- }
- return true;
+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);
}
-template <typename T>
-bool process_server_socket_ssl(
- const std::atomic<socket_t> &svr_sock, SSL *ssl, 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, ssl, read_timeout_sec, read_timeout_usec,
- write_timeout_sec, write_timeout_usec);
- return callback(strm, close_connection, connection_closed);
- });
+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);
}
-template <typename T>
-bool process_client_socket_ssl(
- SSL *ssl, 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<std::chrono::steady_clock> start_time, T callback) {
- SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec,
- write_timeout_sec, write_timeout_usec, max_timeout_msec,
- start_time);
- return callback(strm);
+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");
}
-// SSL socket stream implementation
-SSLSocketStream::SSLSocketStream(
- socket_t sock, SSL *ssl, 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<std::chrono::steady_clock> start_time)
- : sock_(sock), ssl_(ssl), 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) {
- SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY);
+Result ClientImpl::Put(const std::string &path,
+ const UploadFormDataItems &items,
+ UploadProgress progress) {
+ return Put(path, Headers(), items, progress);
}
-SSLSocketStream::~SSLSocketStream() = default;
-
-bool SSLSocketStream::is_readable() const {
- return SSL_pending(ssl_) > 0;
+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);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
+ return Put(path, headers, body, content_type, progress);
}
-bool SSLSocketStream::wait_readable() const {
- if (max_timeout_msec_ <= 0) {
- return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
+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};
}
- 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;
+ const auto &content_type =
+ detail::serialize_multipart_formdata_get_content_type(boundary);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
+ return Put(path, headers, body, content_type, progress);
}
-bool SSLSocketStream::wait_writable() const {
- return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 &&
- is_socket_alive(sock_) && !is_ssl_peer_could_be_closed(ssl_, sock_);
+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);
}
-ssize_t SSLSocketStream::read(char *ptr, size_t size) {
- if (SSL_pending(ssl_) > 0) {
- auto ret = SSL_read(ssl_, ptr, static_cast<int>(size));
- if (ret == 0) { error_ = Error::ConnectionClosed; }
- return ret;
- } else if (wait_readable()) {
- auto ret = SSL_read(ssl_, ptr, static_cast<int>(size));
- if (ret < 0) {
- auto err = SSL_get_error(ssl_, ret);
- auto n = 1000;
-#ifdef _WIN32
- while (--n >= 0 && (err == SSL_ERROR_WANT_READ ||
- (err == SSL_ERROR_SYSCALL &&
- WSAGetLastError() == WSAETIMEDOUT))) {
-#else
- while (--n >= 0 && err == SSL_ERROR_WANT_READ) {
-#endif
- if (SSL_pending(ssl_) > 0) {
- return SSL_read(ssl_, ptr, static_cast<int>(size));
- } else if (wait_readable()) {
- std::this_thread::sleep_for(std::chrono::microseconds{10});
- ret = SSL_read(ssl_, ptr, static_cast<int>(size));
- if (ret >= 0) { return ret; }
- err = SSL_get_error(ssl_, ret);
- } else {
- break;
- }
- }
- assert(ret < 0);
- } else if (ret == 0) {
- error_ = Error::ConnectionClosed;
- }
- return ret;
- } else {
- error_ = Error::Timeout;
- return -1;
- }
+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);
}
-ssize_t SSLSocketStream::write(const char *ptr, size_t size) {
- if (wait_writable()) {
- auto handle_size = static_cast<int>(
- std::min<size_t>(size, (std::numeric_limits<int>::max)()));
-
- auto ret = SSL_write(ssl_, ptr, static_cast<int>(handle_size));
- if (ret < 0) {
- auto err = SSL_get_error(ssl_, ret);
- auto n = 1000;
-#ifdef _WIN32
- while (--n >= 0 && (err == SSL_ERROR_WANT_WRITE ||
- (err == SSL_ERROR_SYSCALL &&
- WSAGetLastError() == WSAETIMEDOUT))) {
-#else
- while (--n >= 0 && err == SSL_ERROR_WANT_WRITE) {
-#endif
- if (wait_writable()) {
- std::this_thread::sleep_for(std::chrono::microseconds{10});
- ret = SSL_write(ssl_, ptr, static_cast<int>(handle_size));
- if (ret >= 0) { return ret; }
- err = SSL_get_error(ssl_, ret);
- } else {
- break;
- }
- }
- assert(ret < 0);
- }
- return ret;
- }
- return -1;
+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);
}
-void SSLSocketStream::get_remote_ip_and_port(std::string &ip,
- int &port) const {
- detail::get_remote_ip_and_port(sock_, ip, port);
+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);
}
-void SSLSocketStream::get_local_ip_and_port(std::string &ip,
- int &port) const {
- detail::get_local_ip_and_port(sock_, ip, port);
+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);
}
-socket_t SSLSocketStream::socket() const { return sock_; }
+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);
+}
-time_t SSLSocketStream::duration() const {
- return std::chrono::duration_cast<std::chrono::milliseconds>(
- std::chrono::steady_clock::now() - start_time_)
- .count();
+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);
}
-} // namespace detail
+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);
-// 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) {
- ctx_ = SSL_CTX_new(TLS_server_method());
+ if (max_timeout_msec_ > 0) {
+ req.start_time_ = std::chrono::steady_clock::now();
+ }
- if (ctx_) {
- SSL_CTX_set_options(ctx_,
- SSL_OP_NO_COMPRESSION |
- SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
+ if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
- SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION);
+ return send_(std::move(req));
+}
- if (private_key_password != nullptr && (private_key_password[0] != '\0')) {
- SSL_CTX_set_default_passwd_cb_userdata(
- ctx_,
- reinterpret_cast<void *>(const_cast<char *>(private_key_password)));
- }
+Result ClientImpl::Patch(const std::string &path) {
+ return Patch(path, std::string(), std::string());
+}
- if (SSL_CTX_use_certificate_chain_file(ctx_, cert_path) != 1 ||
- SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) !=
- 1 ||
- SSL_CTX_check_private_key(ctx_) != 1) {
- last_ssl_error_ = static_cast<int>(ERR_get_error());
- SSL_CTX_free(ctx_);
- ctx_ = nullptr;
- } else if (client_ca_cert_file_path || client_ca_cert_dir_path) {
- SSL_CTX_load_verify_locations(ctx_, client_ca_cert_file_path,
- client_ca_cert_dir_path);
-
- // Set client CA list to be sent to clients during TLS handshake
- if (client_ca_cert_file_path) {
- auto ca_list = SSL_load_client_CA_file(client_ca_cert_file_path);
- if (ca_list != nullptr) {
- SSL_CTX_set_client_CA_list(ctx_, ca_list);
- } else {
- // Failed to load client CA list, but we continue since
- // SSL_CTX_load_verify_locations already succeeded and
- // certificate verification will still work
- last_ssl_error_ = static_cast<int>(ERR_get_error());
- }
- }
+Result ClientImpl::Patch(const std::string &path, const Headers &headers,
+ UploadProgress progress) {
+ return Patch(path, headers, nullptr, 0, std::string(), progress);
+}
- SSL_CTX_set_verify(
- ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
- }
- }
+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);
}
-SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key,
- X509_STORE *client_ca_cert_store) {
- ctx_ = SSL_CTX_new(TLS_server_method());
+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);
+}
- if (ctx_) {
- SSL_CTX_set_options(ctx_,
- SSL_OP_NO_COMPRESSION |
- SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
+Result ClientImpl::Patch(const std::string &path, const Params ¶ms) {
+ return Patch(path, Headers(), params);
+}
- SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION);
+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);
+}
- if (SSL_CTX_use_certificate(ctx_, cert) != 1 ||
- SSL_CTX_use_PrivateKey(ctx_, private_key) != 1) {
- SSL_CTX_free(ctx_);
- ctx_ = nullptr;
- } else if (client_ca_cert_store) {
- SSL_CTX_set_cert_store(ctx_, client_ca_cert_store);
+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);
+}
- // Extract CA names from the store and set them as the client CA list
- auto ca_list = extract_ca_names_from_x509_store(client_ca_cert_store);
- if (ca_list) {
- SSL_CTX_set_client_CA_list(ctx_, ca_list);
- } else {
- // Failed to extract CA names, record the error
- last_ssl_error_ = static_cast<int>(ERR_get_error());
- }
+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);
+}
- SSL_CTX_set_verify(
- ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
- }
- }
+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);
}
-SSLServer::SSLServer(
- const std::function<bool(SSL_CTX &ssl_ctx)> &setup_ssl_ctx_callback) {
- ctx_ = SSL_CTX_new(TLS_method());
- if (ctx_) {
- if (!setup_ssl_ctx_callback(*ctx_)) {
- SSL_CTX_free(ctx_);
- ctx_ = nullptr;
- }
- }
+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");
}
-SSLServer::~SSLServer() {
- if (ctx_) { SSL_CTX_free(ctx_); }
+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);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
+ return Patch(path, headers, body, content_type, progress);
}
-bool SSLServer::is_valid() const { return ctx_; }
+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};
+ }
-SSL_CTX *SSLServer::ssl_context() const { return ctx_; }
+ const auto &content_type =
+ detail::serialize_multipart_formdata_get_content_type(boundary);
+ const auto &body = detail::serialize_multipart_formdata(items, boundary);
+ return Patch(path, headers, body, content_type, progress);
+}
-void SSLServer::update_certs(X509 *cert, EVP_PKEY *private_key,
- X509_STORE *client_ca_cert_store) {
+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);
+}
- std::lock_guard<std::mutex> guard(ctx_mutex_);
+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);
+}
- SSL_CTX_use_certificate(ctx_, cert);
- SSL_CTX_use_PrivateKey(ctx_, private_key);
+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);
+}
- if (client_ca_cert_store != nullptr) {
- SSL_CTX_set_cert_store(ctx_, client_ca_cert_store);
- }
+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);
}
-bool SSLServer::process_and_close_socket(socket_t sock) {
- auto ssl = detail::ssl_new(
- sock, ctx_, ctx_mutex_,
- [&](SSL *ssl2) {
- return detail::ssl_connect_or_accept_nonblocking(
- sock, ssl2, SSL_accept, read_timeout_sec_, read_timeout_usec_,
- &last_ssl_error_);
- },
- [](SSL * /*ssl2*/) { return true; });
+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);
+}
- auto ret = false;
- if (ssl) {
- std::string remote_addr;
- int remote_port = 0;
- detail::get_remote_ip_and_port(sock, remote_addr, remote_port);
+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);
+}
- std::string local_addr;
- int local_port = 0;
- detail::get_local_ip_and_port(sock, local_addr, local_port);
+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);
+}
- ret = detail::process_server_socket_ssl(
- svr_sock_, ssl, 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 = ssl; });
- });
+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);
- // Shutdown gracefully if the result seemed successful, non-gracefully if
- // the connection appeared to be closed.
- const bool shutdown_gracefully = ret;
- detail::ssl_delete(ctx_mutex_, ssl, sock, shutdown_gracefully);
+ if (max_timeout_msec_ > 0) {
+ req.start_time_ = std::chrono::steady_clock::now();
}
- detail::shutdown_socket(sock);
- detail::close_socket(sock);
- return ret;
+ if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
+
+ return send_(std::move(req));
}
-STACK_OF(X509_NAME) * SSLServer::extract_ca_names_from_x509_store(
- X509_STORE *store) {
- if (!store) { return nullptr; }
+Result ClientImpl::Delete(const std::string &path,
+ DownloadProgress progress) {
+ return Delete(path, Headers(), std::string(), std::string(), progress);
+}
- auto ca_list = sk_X509_NAME_new_null();
- if (!ca_list) { return nullptr; }
+Result ClientImpl::Delete(const std::string &path,
+ const Headers &headers,
+ DownloadProgress progress) {
+ return Delete(path, headers, std::string(), std::string(), progress);
+}
- // Get all objects from the store
- auto objs = X509_STORE_get0_objects(store);
- if (!objs) {
- sk_X509_NAME_free(ca_list);
- return nullptr;
- }
+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);
+}
- // Iterate through objects and extract certificate subject names
- for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) {
- auto obj = sk_X509_OBJECT_value(objs, i);
- if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
- auto cert = X509_OBJECT_get0_X509(obj);
- if (cert) {
- auto subject = X509_get_subject_name(cert);
- if (subject) {
- auto name_dup = X509_NAME_dup(subject);
- if (name_dup) { sk_X509_NAME_push(ca_list, name_dup); }
- }
- }
- }
- }
+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);
+}
- // If no names were extracted, free the list and return nullptr
- if (sk_X509_NAME_num(ca_list) == 0) {
- sk_X509_NAME_free(ca_list);
- return nullptr;
- }
+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);
+}
- return ca_list;
+Result ClientImpl::Delete(const std::string &path, const Params ¶ms,
+ DownloadProgress progress) {
+ return Delete(path, Headers(), params, progress);
}
-// SSL HTTP client implementation
-SSLClient::SSLClient(const std::string &host)
- : SSLClient(host, 443, std::string(), std::string()) {}
+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);
+}
-SSLClient::SSLClient(const std::string &host, int port)
- : SSLClient(host, port, std::string(), std::string()) {}
+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();
+ }
-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) {
- ctx_ = SSL_CTX_new(TLS_client_method());
+ if (!content_type.empty()) { req.set_header("Content-Type", content_type); }
+ req.body.assign(body, content_length);
- SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION);
+ return send_(std::move(req));
+}
- detail::split(&host_[0], &host_[host_.size()], '.',
- [&](const char *b, const char *e) {
- host_components_.emplace_back(b, e);
- });
+Result ClientImpl::Options(const std::string &path) {
+ return Options(path, Headers());
+}
- if (!client_cert_path.empty() && !client_key_path.empty()) {
- if (!private_key_password.empty()) {
- SSL_CTX_set_default_passwd_cb_userdata(
- ctx_, reinterpret_cast<void *>(
- const_cast<char *>(private_key_password.c_str())));
- }
-
- if (SSL_CTX_use_certificate_file(ctx_, client_cert_path.c_str(),
- SSL_FILETYPE_PEM) != 1 ||
- SSL_CTX_use_PrivateKey_file(ctx_, client_key_path.c_str(),
- SSL_FILETYPE_PEM) != 1) {
- last_openssl_error_ = ERR_get_error();
- SSL_CTX_free(ctx_);
- ctx_ = nullptr;
- }
+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();
}
-}
-SSLClient::SSLClient(const std::string &host, int port,
- X509 *client_cert, EVP_PKEY *client_key,
- const std::string &private_key_password)
- : ClientImpl(host, port) {
- ctx_ = SSL_CTX_new(TLS_client_method());
+ return send_(std::move(req));
+}
- detail::split(&host_[0], &host_[host_.size()], '.',
- [&](const char *b, const char *e) {
- host_components_.emplace_back(b, e);
- });
+void ClientImpl::stop() {
+ std::lock_guard<std::mutex> guard(socket_mutex_);
- if (client_cert != nullptr && client_key != nullptr) {
- if (!private_key_password.empty()) {
- SSL_CTX_set_default_passwd_cb_userdata(
- ctx_, reinterpret_cast<void *>(
- const_cast<char *>(private_key_password.c_str())));
- }
+ // 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_);
- if (SSL_CTX_use_certificate(ctx_, client_cert) != 1 ||
- SSL_CTX_use_PrivateKey(ctx_, client_key) != 1) {
- last_openssl_error_ = ERR_get_error();
- SSL_CTX_free(ctx_);
- ctx_ = nullptr;
- }
+ // 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;
}
+
+ // Otherwise, still holding the mutex, we can shut everything down ourselves
+ shutdown_ssl(socket_, true);
+ shutdown_socket(socket_);
+ close_socket(socket_);
}
-SSLClient::~SSLClient() {
- if (ctx_) { SSL_CTX_free(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);
+std::string ClientImpl::host() const { return host_; }
+
+int ClientImpl::port() const { return port_; }
+
+size_t ClientImpl::is_socket_open() const {
+ std::lock_guard<std::mutex> guard(socket_mutex_);
+ return socket_.is_open();
}
-bool SSLClient::is_valid() const { return ctx_; }
+socket_t ClientImpl::socket() const { return socket_.sock; }
-void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) {
- if (ca_cert_store) {
- if (ctx_) {
- if (SSL_CTX_get_cert_store(ctx_) != ca_cert_store) {
- // Free memory allocated for old cert and use new store
- // `ca_cert_store`
- SSL_CTX_set_cert_store(ctx_, ca_cert_store);
- ca_cert_store_ = ca_cert_store;
- }
- } else {
- X509_STORE_free(ca_cert_store);
- }
- }
+void ClientImpl::set_connection_timeout(time_t sec, time_t usec) {
+ connection_timeout_sec_ = sec;
+ connection_timeout_usec_ = usec;
}
-void SSLClient::load_ca_cert_store(const char *ca_cert,
- std::size_t size) {
- set_ca_cert_store(ClientImpl::create_ca_cert_store(ca_cert, size));
+void ClientImpl::set_read_timeout(time_t sec, time_t usec) {
+ read_timeout_sec_ = sec;
+ read_timeout_usec_ = usec;
}
-long SSLClient::get_openssl_verify_result() const {
- return verify_result_;
+void ClientImpl::set_write_timeout(time_t sec, time_t usec) {
+ write_timeout_sec_ = sec;
+ write_timeout_usec_ = usec;
}
-SSL_CTX *SSLClient::ssl_context() const { return ctx_; }
+void ClientImpl::set_max_timeout(time_t msec) {
+ max_timeout_msec_ = msec;
+}
-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);
+void ClientImpl::set_basic_auth(const std::string &username,
+ const std::string &password) {
+ basic_auth_username_ = username;
+ basic_auth_password_ = password;
}
-// 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<std::chrono::steady_clock> 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;
- }
+void ClientImpl::set_bearer_token_auth(const std::string &token) {
+ bearer_token_auth_token_ = token;
+}
- if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407) {
- if (!proxy_digest_auth_username_.empty() &&
- !proxy_digest_auth_password_.empty()) {
- std::map<std::string, std::string> 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);
+void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; }
- // Create a new socket for the authenticated CONNECT request
- if (!ensure_socket_connection(socket, error)) {
- success = false;
- output_error_log(error, nullptr);
- return false;
- }
+void ClientImpl::set_follow_location(bool on) { follow_location_ = on; }
- 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;
- }
- }
- }
- }
+void ClientImpl::set_path_encode(bool on) { path_encode_ = on; }
- // 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;
+void
+ClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
+ addr_map_ = std::move(addr_map);
}
-bool SSLClient::load_certs() {
- auto ret = true;
-
- std::call_once(initialize_cert_, [&]() {
- std::lock_guard<std::mutex> guard(ctx_mutex_);
- if (!ca_cert_file_path_.empty()) {
- if (!SSL_CTX_load_verify_locations(ctx_, ca_cert_file_path_.c_str(),
- nullptr)) {
- last_openssl_error_ = ERR_get_error();
- ret = false;
- }
- } else if (!ca_cert_dir_path_.empty()) {
- if (!SSL_CTX_load_verify_locations(ctx_, nullptr,
- ca_cert_dir_path_.c_str())) {
- last_openssl_error_ = ERR_get_error();
- ret = false;
- }
- } else if (!ca_cert_store_) {
- auto loaded = false;
-#ifdef _WIN32
- loaded =
- detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
-#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC
- loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_));
-#endif // _WIN32
- if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); }
- }
- });
-
- return ret;
+void ClientImpl::set_default_headers(Headers headers) {
+ default_headers_ = std::move(headers);
}
-bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
- auto ssl = detail::ssl_new(
- socket.sock, ctx_, ctx_mutex_,
- [&](SSL *ssl2) {
- if (server_certificate_verification_) {
- if (!load_certs()) {
- error = Error::SSLLoadingCerts;
- output_error_log(error, nullptr);
- return false;
- }
- SSL_set_verify(ssl2, SSL_VERIFY_NONE, nullptr);
- }
+void ClientImpl::set_header_writer(
+ std::function<ssize_t(Stream &, Headers &)> const &writer) {
+ header_writer_ = writer;
+}
- if (!detail::ssl_connect_or_accept_nonblocking(
- socket.sock, ssl2, SSL_connect, connection_timeout_sec_,
- connection_timeout_usec_, &last_ssl_error_)) {
- error = Error::SSLConnection;
- output_error_log(error, nullptr);
- return false;
- }
+void ClientImpl::set_address_family(int family) {
+ address_family_ = family;
+}
- if (server_certificate_verification_) {
- auto verification_status = SSLVerifierResponse::NoDecisionMade;
+void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; }
- if (server_certificate_verifier_) {
- verification_status = server_certificate_verifier_(ssl2);
- }
+void ClientImpl::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; }
- if (verification_status == SSLVerifierResponse::CertificateRejected) {
- last_openssl_error_ = ERR_get_error();
- error = Error::SSLServerVerification;
- output_error_log(error, nullptr);
- return false;
- }
+void ClientImpl::set_socket_options(SocketOptions socket_options) {
+ socket_options_ = std::move(socket_options);
+}
- if (verification_status == SSLVerifierResponse::NoDecisionMade) {
- verify_result_ = SSL_get_verify_result(ssl2);
+void ClientImpl::set_compress(bool on) { compress_ = on; }
- if (verify_result_ != X509_V_OK) {
- last_openssl_error_ = static_cast<unsigned long>(verify_result_);
- error = Error::SSLServerVerification;
- output_error_log(error, nullptr);
- return false;
- }
+void ClientImpl::set_decompress(bool on) { decompress_ = on; }
- auto server_cert = SSL_get1_peer_certificate(ssl2);
- auto se = detail::scope_exit([&] { X509_free(server_cert); });
+void ClientImpl::set_payload_max_length(size_t length) {
+ payload_max_length_ = length;
+ has_payload_max_length_ = true;
+}
- if (server_cert == nullptr) {
- last_openssl_error_ = ERR_get_error();
- error = Error::SSLServerVerification;
- output_error_log(error, nullptr);
- return false;
- }
+void ClientImpl::set_interface(const std::string &intf) {
+ interface_ = intf;
+}
- if (server_hostname_verification_) {
- if (!verify_host(server_cert)) {
- last_openssl_error_ = X509_V_ERR_HOSTNAME_MISMATCH;
- error = Error::SSLServerHostnameVerification;
- output_error_log(error, nullptr);
- return false;
- }
- }
- }
- }
+void ClientImpl::set_proxy(const std::string &host, int port) {
+ proxy_host_ = host;
+ proxy_port_ = port;
+}
- return true;
- },
- [&](SSL *ssl2) {
- // Set SNI only if host is not IP address
- if (!detail::is_ip_address(host_)) {
-#if defined(OPENSSL_IS_BORINGSSL)
- SSL_set_tlsext_host_name(ssl2, host_.c_str());
-#else
- // NOTE: Direct call instead of using the OpenSSL macro to suppress
- // -Wold-style-cast warning
- SSL_ctrl(ssl2, SSL_CTRL_SET_TLSEXT_HOSTNAME,
- TLSEXT_NAMETYPE_host_name,
- static_cast<void *>(const_cast<char *>(host_.c_str())));
-#endif
- }
- return 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;
+}
- if (ssl) {
- socket.ssl = ssl;
- return true;
- }
+void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) {
+ proxy_bearer_token_auth_token_ = token;
+}
- if (ctx_ == nullptr) {
- error = Error::SSLConnection;
- last_openssl_error_ = ERR_get_error();
- }
+#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;
+}
- shutdown_socket(socket);
- close_socket(socket);
- return false;
+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 SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) {
- shutdown_ssl_impl(socket, shutdown_gracefully);
+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 SSLClient::shutdown_ssl_impl(Socket &socket,
- bool shutdown_gracefully) {
- if (socket.sock == INVALID_SOCKET) {
- assert(socket.ssl == nullptr);
- return;
- }
- if (socket.ssl) {
- detail::ssl_delete(ctx_mutex_, socket.ssl, socket.sock,
- shutdown_gracefully);
- socket.ssl = nullptr;
- }
- assert(socket.ssl == nullptr);
+void ClientImpl::enable_server_certificate_verification(bool enabled) {
+ server_certificate_verification_ = enabled;
}
-bool SSLClient::process_socket(
- const Socket &socket,
- std::chrono::time_point<std::chrono::steady_clock> start_time,
- std::function<bool(Stream &strm)> 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));
+void ClientImpl::enable_server_hostname_verification(bool enabled) {
+ server_hostname_verification_ = enabled;
}
+#endif
-bool SSLClient::is_ssl() const { return true; }
+// ClientImpl::set_ca_cert_store is defined after TLS namespace (uses helpers)
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+X509_STORE *ClientImpl::create_ca_cert_store(const char *ca_cert,
+ std::size_t size) const {
+ auto mem = BIO_new_mem_buf(ca_cert, static_cast<int>(size));
+ auto se = detail::scope_exit([&] { BIO_free_all(mem); });
+ if (!mem) { return nullptr; }
-bool SSLClient::verify_host(X509 *server_cert) const {
- /* Quote from RFC2818 section 3.1 "Server Identity"
+ auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr);
+ if (!inf) { return nullptr; }
- If a subjectAltName extension of type dNSName is present, that MUST
- be used as the identity. Otherwise, the (most specific) Common Name
- field in the Subject field of the certificate MUST be used. Although
- the use of the Common Name is existing practice, it is deprecated and
- Certification Authorities are encouraged to use the dNSName instead.
+ auto cts = X509_STORE_new();
+ if (cts) {
+ for (auto i = 0; i < static_cast<int>(sk_X509_INFO_num(inf)); i++) {
+ auto itmp = sk_X509_INFO_value(inf, i);
+ if (!itmp) { continue; }
- Matching is performed using the matching rules specified by
- [RFC2459]. If more than one identity of a given type is present in
- the certificate (e.g., more than one dNSName name, a match in any one
- of the set is considered acceptable.) Names may contain the wildcard
- character * which is considered to match any single domain name
- component or component fragment. E.g., *.a.com matches foo.a.com but
- not bar.foo.a.com. f*.com matches foo.com but not bar.com.
+ if (itmp->x509) { X509_STORE_add_cert(cts, itmp->x509); }
+ if (itmp->crl) { X509_STORE_add_crl(cts, itmp->crl); }
+ }
+ }
- In some cases, the URI is specified as an IP address rather than a
- hostname. In this case, the iPAddress subjectAltName must be present
- in the certificate and must exactly match the IP in the URI.
+ sk_X509_INFO_pop_free(inf, X509_INFO_free);
+ return cts;
+}
- */
- return verify_host_with_subject_alt_name(server_cert) ||
- verify_host_with_common_name(server_cert);
+void ClientImpl::set_server_certificate_verifier(
+ std::function<SSLVerifierResponse(SSL *ssl)> /*verifier*/) {
+ // Base implementation does nothing - SSLClient overrides this
}
+#endif
-bool
-SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {
- auto ret = false;
+void ClientImpl::set_logger(Logger logger) {
+ logger_ = std::move(logger);
+}
- auto type = GEN_DNS;
+void ClientImpl::set_error_logger(ErrorLogger error_logger) {
+ error_logger_ = std::move(error_logger);
+}
- struct in6_addr addr6 = {};
- struct in_addr addr = {};
- size_t addr_len = 0;
+/*
+ * SSL/TLS Common Implementation
+ */
-#ifndef __MINGW32__
- if (inet_pton(AF_INET6, host_.c_str(), &addr6)) {
- type = GEN_IPADD;
- addr_len = sizeof(struct in6_addr);
- } else if (inet_pton(AF_INET, host_.c_str(), &addr)) {
- type = GEN_IPADD;
- addr_len = sizeof(struct in_addr);
+ClientConnection::~ClientConnection() {
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ if (session) {
+ tls::shutdown(session, true);
+ tls::free_session(session);
+ session = nullptr;
}
#endif
- auto alt_names = static_cast<const struct stack_st_GENERAL_NAME *>(
- X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr));
-
- if (alt_names) {
- auto dsn_matched = false;
- auto ip_matched = false;
-
- auto count = sk_GENERAL_NAME_num(alt_names);
-
- for (decltype(count) i = 0; i < count && !dsn_matched; i++) {
- auto val = sk_GENERAL_NAME_value(alt_names, i);
- if (!val || val->type != type) { continue; }
+ if (sock != INVALID_SOCKET) {
+ detail::close_socket(sock);
+ sock = INVALID_SOCKET;
+ }
+}
- auto name =
- reinterpret_cast<const char *>(ASN1_STRING_get0_data(val->d.ia5));
- if (name == nullptr) { continue; }
+// Universal client implementation
+Client::Client(const std::string &scheme_host_port)
+ : Client(scheme_host_port, std::string(), std::string()) {}
- auto name_len = static_cast<size_t>(ASN1_STRING_length(val->d.ia5));
+Client::Client(const std::string &scheme_host_port,
+ const std::string &client_cert_path,
+ const std::string &client_key_path) {
+ const static std::regex re(
+ R"((?:([a-z]+):\/\/)?(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)");
- switch (type) {
- case GEN_DNS: dsn_matched = check_host_name(name, name_len); break;
+ std::smatch m;
+ if (std::regex_match(scheme_host_port, m, re)) {
+ auto scheme = m[1].str();
- case GEN_IPADD:
- if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) {
- ip_matched = true;
- }
- break;
- }
+#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;
}
- if (dsn_matched || ip_matched) { ret = true; }
- }
+ auto is_ssl = scheme == "https";
- GENERAL_NAMES_free(const_cast<STACK_OF(GENERAL_NAME) *>(
- reinterpret_cast<const STACK_OF(GENERAL_NAME) *>(alt_names)));
- return ret;
-}
+ auto host = m[2].str();
+ if (host.empty()) { host = m[3].str(); }
-bool SSLClient::verify_host_with_common_name(X509 *server_cert) const {
- const auto subject_name = X509_get_subject_name(server_cert);
-
- if (subject_name != nullptr) {
- char name[BUFSIZ];
- auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName,
- name, sizeof(name));
-
- if (name_len != -1) {
- return check_host_name(name, static_cast<size_t>(name_len));
- }
- }
-
- return false;
-}
-
-bool SSLClient::check_host_name(const char *pattern,
- size_t pattern_len) const {
- // Exact match (case-insensitive)
- if (host_.size() == pattern_len &&
- detail::case_ignore::equal(host_, std::string(pattern, pattern_len))) {
- return true;
- }
-
- // Wildcard match
- // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484
- std::vector<std::string> pattern_components;
- detail::split(&pattern[0], &pattern[pattern_len], '.',
- [&](const char *b, const char *e) {
- pattern_components.emplace_back(b, e);
- });
-
- if (host_components_.size() != pattern_components.size()) { return false; }
-
- auto itr = pattern_components.begin();
- for (const auto &h : host_components_) {
- auto &p = *itr;
- if (!httplib::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<std::string::difference_type>(
- prefix_length),
- h.begin(), [](const char ca, const char cb) {
- return httplib::detail::case_ignore::to_lower(ca) ==
- httplib::detail::case_ignore::to_lower(cb);
- });
- }
- }
- if (!partial_match) { return false; }
- }
- ++itr;
- }
-
- return true;
-}
-#endif
-
-// 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) {
- const static std::regex re(
- R"((?:([a-z]+):\/\/)?(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)");
-
- std::smatch m;
- if (std::regex_match(scheme_host_port, m, re)) {
- auto scheme = m[1].str();
-
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
- 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 = m[2].str();
- if (host.empty()) { host = m[3].str(); }
-
- auto port_str = m[4].str();
- auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);
+ auto port_str = m[4].str();
+ auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);
if (is_ssl) {
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+#ifdef CPPHTTPLIB_SSL_ENABLED
cli_ = detail::make_unique<SSLClient>(host, port, client_cert_path,
client_key_path);
is_ssl_ = is_ssl;
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::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<std::string, std::string> 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<ssize_t(Stream &, Headers &)> 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); }
+
+[[deprecated("Use set_path_encode instead")]]
+void Client::set_url_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_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<int>(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<int>(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<int>(get_error());
+ free_context(ctx_);
+ ctx_ = nullptr;
+ } else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) {
+ if (!load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) {
+ last_ssl_error_ = static_cast<int>(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<std::mutex> guard(ctx_mutex_);
+ session = create_session(static_cast<ctx_t>(ctx_), sock);
+ }
+
+ if (!session) {
+ last_ssl_error_ = static_cast<int>(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;
+ auto cleanup = detail::scope_exit([&] {
+ // Shutdown gracefully if handshake succeeded and processing was successful
+ if (handshake_done) { shutdown(session, 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<int>(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; });
+ });
+
+ 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<std::mutex> 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<std::mutex> 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<std::chrono::steady_clock> start_time,
+ std::function<bool(Stream &strm)> 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);
+}
+
+// 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<std::chrono::steady_clock> 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<std::string, std::string> 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 (!proxy_host_.empty() && proxy_port_ != -1) { 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()) {}
+
+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) {
+ ctx_ = tls::create_client_context();
+ if (!ctx_) { return; }
+
+ tls::set_min_version(ctx_, tls::Version::TLS1_2);
+
+ 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)) {
+ last_backend_error_ = tls::get_error();
+ tls::free_context(ctx_);
+ ctx_ = nullptr;
+ }
+ }
+}
+
+SSLClient::SSLClient(const std::string &host, int port,
+ const PemMemory &pem)
+ : ClientImpl(host, port) {
+ ctx_ = tls::create_client_context();
+ if (!ctx_) { return; }
+
+ tls::set_min_version(ctx_, tls::Version::TLS1_2);
+
+ if (pem.cert_pem && pem.key_pem) {
+ if (!tls::set_client_cert_pem(ctx_, pem.cert_pem, pem.key_pem,
+ pem.private_key_password)) {
+ last_backend_error_ = tls::get_error();
+ tls::free_context(ctx_);
+ ctx_ = nullptr;
+ }
+ }
+}
+
+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);
+ } 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<SSLVerifierResponse(tls::session_t)> verifier) {
+ session_verifier_ = std::move(verifier);
+}
+
+#if defined(_WIN32) && \
+ !defined(CPPHTTPLIB_DISABLE_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<std::mutex> guard(ctx_mutex_);
+
+ if (!ca_cert_file_path_.empty()) {
+ if (!tls::load_ca_file(ctx_, ca_cert_file_path_.c_str())) {
+ last_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())) {
+ last_backend_error_ = tls::get_error();
+ ret = false;
+ }
+ } else if (ca_cert_pem_.empty()) {
+ if (!tls::load_system_certs(ctx_)) {
+ last_backend_error_ = tls::get_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_);
+
+#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT
+ // MbedTLS needs explicit verification mode (OpenSSL uses SSL_VERIFY_NONE
+ // by default and performs all verification post-handshake).
+ // For IP addresses with verification enabled, use OPTIONAL mode since
+ // MbedTLS requires hostname for VERIFY_REQUIRED.
+ if (is_ip && server_certificate_verification_) {
+ set_verify_client(ctx_, false);
+ } else {
+ set_verify_client(ctx_, server_certificate_verification_);
+ }
+#endif
+
+ // Create TLS session
+ session_t session = nullptr;
+ {
+ std::lock_guard<std::mutex> guard(ctx_mutex_);
+ session = create_session(ctx_, socket.sock);
+ }
+
+ if (!session) {
+ error = Error::SSLConnection;
+ last_backend_error_ = get_error();
+ return false;
+ }
+
+ // 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<int>(tls_err.code);
+ last_backend_error_ = tls_err.backend_code;
+ if (tls_err.code == ErrorCode::CertVerifyFailed) {
+ error = Error::SSLServerVerification;
+ } else if (tls_err.code == ErrorCode::HostnameMismatch) {
+ error = Error::SSLServerHostnameVerification;
+ } else {
+ error = Error::SSLConnection;
+ }
+ output_error_log(error, nullptr);
+ return false;
+ }
+
+ // Post-handshake session verifier callback
+ auto verification_status = SSLVerifierResponse::NoDecisionMade;
+ if (session_verifier_) { verification_status = session_verifier_(session); }
+
+ if (verification_status == SSLVerifierResponse::CertificateRejected) {
+ last_backend_error_ = get_error();
+ error = Error::SSLServerVerification;
+ 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<unsigned long>(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;
+ }
+ }
+
+#if defined(_WIN32) && \
+ !defined(CPPHTTPLIB_DISABLE_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.
+ if (enable_windows_cert_verification_ && ca_cert_file_path_.empty() &&
+ ca_cert_dir_path_.empty() && ca_cert_pem_.empty()) {
+ std::vector<unsigned char> der;
+ if (get_cert_der(server_cert, der)) {
+ unsigned long 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);
+}
+
+#if defined(_WIN32) && \
+ !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+void Client::enable_windows_certificate_verification(bool enabled) {
+ if (is_ssl_) {
+ static_cast<SSLClient &>(*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<SSLClient &>(*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) {
+ set_ca_cert_store(tls::create_ca_store(ca_cert, size));
+}
+
+void
+Client::set_server_certificate_verifier(tls::VerifyCallback verifier) {
+ if (is_ssl_) {
+ static_cast<SSLClient &>(*cli_).set_server_certificate_verifier(
+ std::move(verifier));
+ }
+}
+
+void Client::set_session_verifier(
+ std::function<SSLVerifierResponse(tls::session_t)> verifier) {
+ if (is_ssl_) {
+ static_cast<SSLClient &>(*cli_).set_session_verifier(std::move(verifier));
+ }
+}
+
+tls::ctx_t Client::tls_context() const {
+ if (is_ssl_) { return static_cast<SSLClient &>(*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;
+}
+
+} // 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<SSL_CTX *>(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<SanEntry> PeerCert::sans() const {
+ std::vector<SanEntry> 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<SanEntry> VerifyContext::sans() const {
+ std::vector<SanEntry> 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
+ */
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+SSL_CTX *Client::ssl_context() const {
+ if (is_ssl_) { return static_cast<SSLClient &>(*cli_).ssl_context(); }
+ return nullptr;
+}
+
+void Client::set_server_certificate_verifier(
+ std::function<SSLVerifierResponse(SSL *ssl)> verifier) {
+ cli_->set_server_certificate_verifier(verifier);
+}
+
+long Client::get_verify_result() const {
+ if (is_ssl_) { return static_cast<SSLClient &>(*cli_).get_verify_result(); }
+ return -1; // NOTE: -1 doesn't match any of X509_V_ERR_???
+}
+#endif // CPPHTTPLIB_OPENSSL_SUPPORT
+
+/*
+ * OpenSSL Backend Implementation
+ */
+
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+namespace tls {
+
+namespace impl {
+
+// OpenSSL-specific helpers for converting native types to PEM
+std::string x509_to_pem(X509 *cert) {
+ if (!cert) return {};
+ BIO *bio = BIO_new(BIO_s_mem());
+ if (!bio) return {};
+ if (PEM_write_bio_X509(bio, cert) != 1) {
+ BIO_free(bio);
+ return {};
+ }
+ char *data = nullptr;
+ long len = BIO_get_mem_data(bio, &data);
+ std::string pem(data, static_cast<size_t>(len));
+ BIO_free(bio);
+ return pem;
+}
+
+std::string evp_pkey_to_pem(EVP_PKEY *key) {
+ if (!key) return {};
+ BIO *bio = BIO_new(BIO_s_mem());
+ if (!bio) return {};
+ if (PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr,
+ nullptr) != 1) {
+ BIO_free(bio);
+ return {};
+ }
+ char *data = nullptr;
+ long len = BIO_get_mem_data(bio, &data);
+ std::string pem(data, static_cast<size_t>(len));
+ BIO_free(bio);
+ return pem;
+}
+
+std::string x509_store_to_pem(X509_STORE *store) {
+ if (!store) return {};
+ std::string pem;
+ auto objs = X509_STORE_get0_objects(store);
+ if (!objs) return {};
+ auto count = sk_X509_OBJECT_num(objs);
+ for (decltype(count) i = 0; i < count; i++) {
+ auto obj = sk_X509_OBJECT_value(objs, i);
+ if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
+ auto cert = X509_OBJECT_get0_X509(obj);
+ if (cert) { pem += x509_to_pem(cert); }
+ }
+ }
+ return pem;
+}
+
+// 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) {
+ X509_NAME *name = X509_get_subject_name(cert);
+ if (name) { sk_X509_NAME_push(ca_list, X509_NAME_dup(name)); }
+ X509_free(cert);
+ }
+ BIO_free(bio);
+
+ return ca_list;
+}
+
+// Helper: Extract CA names from X509_STORE
+// Returns a new STACK_OF(X509_NAME)* or nullptr on failure
+// Caller takes ownership of returned list
+STACK_OF(X509_NAME) *
+ extract_client_ca_list_from_store(X509_STORE *store) {
+ if (!store) { return nullptr; }
+
+ auto ca_list = sk_X509_NAME_new_null();
+ if (!ca_list) { return nullptr; }
+
+ auto objs = X509_STORE_get0_objects(store);
+ if (!objs) {
+ sk_X509_NAME_free(ca_list);
+ return nullptr;
+ }
+
+ auto count = sk_X509_OBJECT_num(objs);
+ for (decltype(count) i = 0; i < count; i++) {
+ auto obj = sk_X509_OBJECT_value(objs, i);
+ if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
+ auto cert = X509_OBJECT_get0_X509(obj);
+ if (cert) {
+ auto subject = X509_get_subject_name(cert);
+ if (subject) {
+ auto name_dup = X509_NAME_dup(subject);
+ if (name_dup) { sk_X509_NAME_push(ca_list, name_dup); }
+ }
+ }
+ }
+ }
+
+ if (sk_X509_NAME_num(ca_list) == 0) {
+ sk_X509_NAME_free(ca_list);
+ return nullptr;
+ }
+
+ 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<SSL *>(
+ 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<session_t>(ssl);
+ verify_ctx.cert = static_cast<cert_t>(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;
+}
+
+} // 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_t>(ctx);
+}
+
+void free_context(ctx_t ctx) {
+ if (ctx) { SSL_CTX_free(static_cast<SSL_CTX *>(ctx)); }
+}
+
+bool set_min_version(ctx_t ctx, Version version) {
+ if (!ctx) return false;
+ return SSL_CTX_set_min_proto_version(static_cast<SSL_CTX *>(ctx),
+ static_cast<int>(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<SSL_CTX *>(ctx);
+ auto store = SSL_CTX_get_cert_store(ssl_ctx);
+ if (!store) return false;
+
+ auto bio = BIO_new_mem_buf(pem, static_cast<int>(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<SSL_CTX *>(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<SSL_CTX *>(ctx), nullptr,
+ dir_path) == 1;
+}
+
+bool load_system_certs(ctx_t ctx) {
+ if (!ctx) return false;
+ auto ssl_ctx = static_cast<SSL_CTX *>(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;
+
+ CFArrayRef certs = nullptr;
+ if (SecTrustCopyAnchorCertificates(&certs) != errSecSuccess || !certs) {
+ return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
+ }
+
+ bool loaded_any = false;
+ auto count = CFArrayGetCount(certs);
+ for (CFIndex i = 0; i < count; i++) {
+ auto cert = reinterpret_cast<SecCertificateRef>(
+ const_cast<void *>(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<SSL_CTX *>(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<char *>(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<SSL_CTX *>(ctx);
+
+ if (password && password[0] != '\0') {
+ SSL_CTX_set_default_passwd_cb_userdata(
+ ssl_ctx, reinterpret_cast<void *>(const_cast<char *>(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_t>(ctx);
+}
+
+void set_verify_client(ctx_t ctx, bool require) {
+ if (!ctx) return;
+ SSL_CTX_set_verify(static_cast<SSL_CTX *>(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<SSL_CTX *>(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<int>(sock), BIO_NOCLOSE);
+ if (!bio) {
+ SSL_free(ssl);
+ return nullptr;
+ }
+
+ SSL_set_bio(ssl, bio, bio);
+ return static_cast<session_t>(ssl);
+}
+
+void free_session(session_t session) {
+ if (session) { SSL_free(static_cast<SSL *>(session)); }
+}
+
+bool set_sni(session_t session, const char *hostname) {
+ if (!session || !hostname) return false;
+
+ auto ssl = static_cast<SSL *>(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<void *>(const_cast<char *>(hostname))) == 1;
+#endif
+}
+
+bool set_hostname(session_t session, const char *hostname) {
+ if (!session || !hostname) return false;
+
+ auto ssl = static_cast<SSL *>(session);
+
+ // Set SNI (Server Name Indication)
+ if (!set_sni(session, hostname)) { return false; }
+
+ // Enable hostname verification
+ auto param = SSL_get0_param(ssl);
+ if (!param) 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<SSL *>(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<SSL *>(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<SSL *>(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<SSL *>(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<SSL *>(session);
+ constexpr auto max_len =
+ static_cast<size_t>((std::numeric_limits<int>::max)());
+ if (len > max_len) { len = max_len; }
+ auto ret = SSL_read(ssl, buf, static_cast<int>(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;
+}
+
+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<SSL *>(session);
+ auto ret = SSL_write(ssl, buf, static_cast<int>(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<SSL *>(const_cast<void *>(session)));
+}
+
+void shutdown(session_t session, bool graceful) {
+ if (!session) return;
+
+ auto ssl = static_cast<SSL *>(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<SSL *>(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<cert_t>(SSL_get1_peer_certificate(
+ static_cast<SSL *>(const_cast<void *>(session))));
+}
+
+void free_cert(cert_t cert) {
+ if (cert) { X509_free(static_cast<X509 *>(cert)); }
+}
+
+bool verify_hostname(cert_t cert, const char *hostname) {
+ if (!cert || !hostname) return false;
+
+ auto x509 = static_cast<X509 *>(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<uint64_t>(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<SSL *>(const_cast<void *>(session)));
+}
+
+std::string get_cert_subject_cn(cert_t cert) {
+ if (!cert) return "";
+ auto x509 = static_cast<X509 *>(cert);
+ auto subject_name = X509_get_subject_name(x509);
+ if (!subject_name) return "";
+
+ char buf[256];
+ auto len =
+ X509_NAME_get_text_by_NID(subject_name, NID_commonName, buf, sizeof(buf));
+ if (len < 0) return "";
+ return std::string(buf, static_cast<size_t>(len));
+}
+
+std::string get_cert_issuer_name(cert_t cert) {
+ if (!cert) return "";
+ auto x509 = static_cast<X509 *>(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<SanEntry> &sans) {
+ sans.clear();
+ if (!cert) return false;
+ auto x509 = static_cast<X509 *>(cert);
+
+ auto names = static_cast<GENERAL_NAMES *>(
+ 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 (int 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<const char *>(
+ ASN1_STRING_get0_data(gen->d.dNSName)),
+ static_cast<size_t>(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<const char *>(
+ ASN1_STRING_get0_data(gen->d.rfc822Name)),
+ static_cast<size_t>(ASN1_STRING_length(gen->d.rfc822Name)));
+ }
+ break;
+ case GEN_URI:
+ entry.type = SanType::URI;
+ if (gen->d.uniformResourceIdentifier) {
+ entry.value = std::string(
+ reinterpret_cast<const char *>(
+ ASN1_STRING_get0_data(gen->d.uniformResourceIdentifier)),
+ static_cast<size_t>(
+ 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<X509 *>(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<X509 *>(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<unsigned char> &der) {
+ if (!cert) return false;
+ auto x509 = static_cast<X509 *>(cert);
+ auto len = i2d_X509(x509, nullptr);
+ if (len < 0) return false;
+ der.resize(static_cast<size_t>(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<SSL *>(const_cast<void *>(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<unsigned long>(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<int>(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<int>(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<ca_store_t>(store);
+}
+
+void free_ca_store(ca_store_t store) {
+ if (store) { X509_STORE_free(static_cast<X509_STORE *>(store)); }
+}
+
+bool set_ca_store(ctx_t ctx, ca_store_t store) {
+ if (!ctx || !store) { return false; }
+ auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+ auto x509_store = static_cast<X509_STORE *>(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<cert_t> &certs) {
+ certs.clear();
+ if (!ctx) { return 0; }
+ auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+ auto store = SSL_CTX_get_cert_store(ssl_ctx);
+ if (!store) { return 0; }
+
+ auto objs = X509_STORE_get0_objects(store);
+ if (!objs) { return 0; }
+
+ 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<cert_t>(x509));
+ }
+ }
+ }
+ return certs.size();
+}
+
+std::vector<std::string> get_ca_names(ctx_t ctx) {
+ std::vector<std::string> names;
+ if (!ctx) { return names; }
+ auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
+
+ auto store = SSL_CTX_get_cert_store(ssl_ctx);
+ if (!store) { return names; }
+
+ auto objs = X509_STORE_get0_objects(store);
+ if (!objs) { return names; }
+
+ 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<SSL_CTX *>(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<char *>(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<SSL_CTX *>(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<X509_STORE *>(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<SSL_CTX *>(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<SSL *>(const_cast<void *>(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<int>(error_code));
+ return str ? str : "unknown error";
+}
+
+namespace impl {
+
+// OpenSSL-specific helpers for public API wrappers
+ctx_t create_server_context_from_x509(X509 *cert, EVP_PKEY *key,
+ X509_STORE *client_ca_store,
+ int &out_error) {
+ out_error = 0;
+ auto cert_pem = x509_to_pem(cert);
+ auto key_pem = evp_pkey_to_pem(key);
+ if (cert_pem.empty() || key_pem.empty()) {
+ out_error = static_cast<int>(ERR_get_error());
+ return nullptr;
+ }
+
+ auto ctx = create_server_context();
+ if (!ctx) {
+ out_error = static_cast<int>(get_error());
+ return nullptr;
+ }
+
+ if (!set_server_cert_pem(ctx, cert_pem.c_str(), key_pem.c_str(), nullptr)) {
+ out_error = static_cast<int>(get_error());
+ free_context(ctx);
+ return nullptr;
+ }
+
+ if (client_ca_store) {
+ // Set cert store for verification (SSL_CTX_set_cert_store takes ownership)
+ SSL_CTX_set_cert_store(static_cast<SSL_CTX *>(ctx), client_ca_store);
+
+ // Extract and set client CA list directly from store (more efficient than
+ // PEM conversion)
+ auto ca_list = extract_client_ca_list_from_store(client_ca_store);
+ if (ca_list) {
+ SSL_CTX_set_client_CA_list(static_cast<SSL_CTX *>(ctx), ca_list);
+ }
+
+ set_verify_client(ctx, true);
+ }
+
+ return ctx;
+}
+
+void update_server_certs_from_x509(ctx_t ctx, X509 *cert, EVP_PKEY *key,
+ X509_STORE *client_ca_store) {
+ auto cert_pem = x509_to_pem(cert);
+ auto key_pem = evp_pkey_to_pem(key);
+
+ if (!cert_pem.empty() && !key_pem.empty()) {
+ update_server_cert(ctx, cert_pem.c_str(), key_pem.c_str(), nullptr);
+ }
+
+ if (client_ca_store) {
+ auto ca_pem = x509_store_to_pem(client_ca_store);
+ if (!ca_pem.empty()) { update_server_client_ca(ctx, ca_pem.c_str()); }
+ X509_STORE_free(client_ca_store);
+ }
+}
+
+ctx_t create_client_context_from_x509(X509 *cert, EVP_PKEY *key,
+ const char *password,
+ unsigned long &out_error) {
+ out_error = 0;
+ auto ctx = create_client_context();
+ if (!ctx) {
+ out_error = static_cast<unsigned long>(get_error());
+ return nullptr;
+ }
+
+ if (cert && key) {
+ auto cert_pem = x509_to_pem(cert);
+ auto key_pem = evp_pkey_to_pem(key);
+ if (cert_pem.empty() || key_pem.empty()) {
+ out_error = ERR_get_error();
+ free_context(ctx);
+ return nullptr;
+ }
+ if (!set_client_cert_pem(ctx, cert_pem.c_str(), key_pem.c_str(),
+ password)) {
+ out_error = static_cast<unsigned long>(get_error());
+ free_context(ctx);
+ return nullptr;
+ }
+ }
+
+ return ctx;
+}
+
+} // namespace impl
+
+} // namespace tls
+
+// ClientImpl::set_ca_cert_store - defined here to use
+// tls::impl::x509_store_to_pem Deprecated: converts X509_STORE to PEM and
+// stores for redirect transfer
+void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) {
+ if (ca_cert_store) {
+ ca_cert_pem_ = tls::impl::x509_store_to_pem(ca_cert_store);
+ }
+}
+
+SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key,
+ X509_STORE *client_ca_cert_store) {
+ ctx_ = tls::impl::create_server_context_from_x509(
+ cert, private_key, client_ca_cert_store, last_ssl_error_);
+}
+
+SSLServer::SSLServer(
+ const std::function<bool(SSL_CTX &ssl_ctx)> &setup_ssl_ctx_callback) {
+ // Use abstract API to create context
+ ctx_ = tls::create_server_context();
+ if (ctx_) {
+ // Pass to OpenSSL-specific callback (ctx_ is SSL_CTX* internally)
+ auto ssl_ctx = static_cast<SSL_CTX *>(ctx_);
+ if (!setup_ssl_ctx_callback(*ssl_ctx)) {
+ tls::free_context(ctx_);
+ ctx_ = nullptr;
+ }
+ }
+}
+
+SSL_CTX *SSLServer::ssl_context() const {
+ return static_cast<SSL_CTX *>(ctx_);
+}
+
+void SSLServer::update_certs(X509 *cert, EVP_PKEY *private_key,
+ X509_STORE *client_ca_cert_store) {
+ std::lock_guard<std::mutex> guard(ctx_mutex_);
+ tls::impl::update_server_certs_from_x509(ctx_, cert, private_key,
+ client_ca_cert_store);
+}
+
+SSLClient::SSLClient(const std::string &host, int port,
+ X509 *client_cert, EVP_PKEY *client_key,
+ const std::string &private_key_password)
+ : ClientImpl(host, port) {
+ const char *password =
+ private_key_password.empty() ? nullptr : private_key_password.c_str();
+ ctx_ = tls::impl::create_client_context_from_x509(
+ client_cert, client_key, password, last_backend_error_);
+}
+
+long SSLClient::get_verify_result() const { return verify_result_; }
+
+void SSLClient::set_server_certificate_verifier(
+ std::function<SSLVerifierResponse(SSL *ssl)> verifier) {
+ // Wrap SSL* callback into backend-independent session_verifier_
+ auto v = std::make_shared<std::function<SSLVerifierResponse(SSL *)>>(
+ std::move(verifier));
+ session_verifier_ = [v](tls::session_t session) {
+ return (*v)(static_cast<SSL *>(session));
+ };
+}
+
+SSL_CTX *SSLClient::ssl_context() const {
+ return static_cast<SSL_CTX *>(ctx_);
+}
+
+bool SSLClient::verify_host(X509 *server_cert) const {
+ /* Quote from RFC2818 section 3.1 "Server Identity"
+
+ If a subjectAltName extension of type dNSName is present, that MUST
+ be used as the identity. Otherwise, the (most specific) Common Name
+ field in the Subject field of the certificate MUST be used. Although
+ the use of the Common Name is existing practice, it is deprecated and
+ Certification Authorities are encouraged to use the dNSName instead.
+
+ Matching is performed using the matching rules specified by
+ [RFC2459]. If more than one identity of a given type is present in
+ the certificate (e.g., more than one dNSName name, a match in any one
+ of the set is considered acceptable.) Names may contain the wildcard
+ character * which is considered to match any single domain name
+ component or component fragment. E.g., *.a.com matches foo.a.com but
+ not bar.foo.a.com. f*.com matches foo.com but not bar.com.
+
+ In some cases, the URI is specified as an IP address rather than a
+ hostname. In this case, the iPAddress subjectAltName must be present
+ in the certificate and must exactly match the IP in the URI.
+
+ */
+ return verify_host_with_subject_alt_name(server_cert) ||
+ verify_host_with_common_name(server_cert);
+}
+
+bool
+SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {
+ auto ret = false;
+
+ auto type = GEN_DNS;
+
+ struct in6_addr addr6 = {};
+ struct in_addr addr = {};
+ size_t addr_len = 0;
+
+#ifndef __MINGW32__
+ if (inet_pton(AF_INET6, host_.c_str(), &addr6)) {
+ type = GEN_IPADD;
+ addr_len = sizeof(struct in6_addr);
+ } else if (inet_pton(AF_INET, host_.c_str(), &addr)) {
+ type = GEN_IPADD;
+ addr_len = sizeof(struct in_addr);
+ }
+#endif
+
+ auto alt_names = static_cast<const struct stack_st_GENERAL_NAME *>(
+ X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr));
+
+ if (alt_names) {
+ auto dsn_matched = false;
+ auto ip_matched = false;
+
+ auto count = sk_GENERAL_NAME_num(alt_names);
+
+ for (decltype(count) i = 0; i < count && !dsn_matched; i++) {
+ auto val = sk_GENERAL_NAME_value(alt_names, i);
+ if (!val || val->type != type) { continue; }
+
+ auto name =
+ reinterpret_cast<const char *>(ASN1_STRING_get0_data(val->d.ia5));
+ if (name == nullptr) { continue; }
+
+ auto name_len = static_cast<size_t>(ASN1_STRING_length(val->d.ia5));
+
+ switch (type) {
+ case GEN_DNS:
+ dsn_matched =
+ detail::match_hostname(std::string(name, name_len), host_);
+ break;
+
+ case GEN_IPADD:
+ if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) {
+ ip_matched = true;
+ }
+ break;
+ }
+ }
+
+ if (dsn_matched || ip_matched) { ret = true; }
+ }
+
+ GENERAL_NAMES_free(const_cast<STACK_OF(GENERAL_NAME) *>(
+ reinterpret_cast<const STACK_OF(GENERAL_NAME) *>(alt_names)));
+ return ret;
+}
+
+bool SSLClient::verify_host_with_common_name(X509 *server_cert) const {
+ const auto subject_name = X509_get_subject_name(server_cert);
+
+ if (subject_name != nullptr) {
+ char name[BUFSIZ];
+ auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName,
+ name, sizeof(name));
+
+ if (name_len != -1) {
+ return detail::match_hostname(
+ std::string(name, static_cast<size_t>(name_len)), host_);
+ }
+ }
+
+ return false;
+}
+
+#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<socket_t *>(ctx);
+#ifdef _WIN32
+ auto ret =
+ send(sock, reinterpret_cast<const char *>(buf), static_cast<int>(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<int>(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<socket_t *>(ctx);
+#ifdef _WIN32
+ auto ret =
+ recv(sock, reinterpret_cast<char *>(buf), static_cast<int>(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<int>(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<const char *>(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);
+
+// 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<unsigned char>(c))) {
+ return false;
+ }
+ }
+ return dots == 3;
+}
+
+// Parse IPv4 address string to bytes
+bool parse_ipv4(const std::string &str, unsigned char *out) {
+ int parts[4];
+ if (sscanf(str.c_str(), "%d.%d.%d.%d", &parts[0], &parts[1], &parts[2],
+ &parts[3]) != 4) {
+ return false;
+ }
+ for (int i = 0; i < 4; i++) {
+ if (parts[i] < 0 || parts[i] > 255) return false;
+ out[i] = static_cast<unsigned char>(parts[i]);
+ }
+ return true;
+}
+
+// MbedTLS verify callback wrapper
+int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt,
+ int cert_depth, uint32_t *flags) {
+ auto &callback = get_verify_callback();
+ if (!callback) { return 0; } // Continue with default verification
+
+ // data points to the MbedTlsSession
+ auto *session = static_cast<MbedTlsSession *>(data);
+
+ // Build context
+ VerifyContext verify_ctx;
+ verify_ctx.session = static_cast<session_t>(session);
+ verify_ctx.cert = static_cast<cert_t>(crt);
+ verify_ctx.depth = cert_depth;
+ verify_ctx.preverify_ok = (*flags == 0);
+ verify_ctx.error_code = static_cast<long>(*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<const unsigned char *>(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_t>(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<const unsigned char *>(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_t>(ctx);
+}
+
+void free_context(ctx_t ctx) {
+ if (ctx) { delete static_cast<impl::MbedTlsContext *>(ctx); }
+}
+
+bool set_min_version(ctx_t ctx, Version version) {
+ if (!ctx) { return false; }
+ auto mctx = static_cast<impl::MbedTlsContext *>(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<impl::MbedTlsContext *>(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<const unsigned char *>(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<impl::MbedTlsContext *>(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<impl::MbedTlsContext *>(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<impl::MbedTlsContext *>(ctx);
+ bool loaded = false;
+
+#ifdef _WIN32
+ // Load from Windows certificate store (ROOT and CA)
+ 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) {
+ int ret = mbedtls_x509_crt_parse_der(
+ &mctx->ca_chain, pContext->pbCertEncoded, pContext->cbCertEncoded);
+ if (ret == 0) { loaded = true; }
+ }
+ CertCloseStore(hStore, 0);
+ }
+ }
+#elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
+ // Load from macOS Keychain
+ CFArrayRef certs = nullptr;
+ OSStatus status = SecTrustCopyAnchorCertificates(&certs);
+ if (status == errSecSuccess && certs) {
+ CFIndex count = CFArrayGetCount(certs);
+ for (CFIndex i = 0; i < count; i++) {
+ SecCertificateRef cert =
+ (SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
+ CFDataRef data = SecCertificateCopyData(cert);
+ if (data) {
+ int ret = mbedtls_x509_crt_parse_der(
+ &mctx->ca_chain, CFDataGetBytePtr(data),
+ static_cast<size_t>(CFDataGetLength(data)));
+ if (ret == 0) { loaded = true; }
+ CFRelease(data);
+ }
+ }
+ CFRelease(certs);
+ }
+#else
+ // Try common CA certificate locations on Linux/Unix
+ static const char *ca_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};
+
+ for (const char **path = ca_paths; *path; ++path) {
+ int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, *path);
+ if (ret >= 0) {
+ loaded = true;
+ break;
+ }
+ }
+
+ // Also try the CA directory
+ if (!loaded) {
+ static const char *ca_dirs[] = {"/etc/ssl/certs", // Debian/Ubuntu
+ "/etc/pki/tls/certs", // RHEL/CentOS
+ "/usr/share/ca-certificates", nullptr};
+
+ for (const char **dir = ca_dirs; *dir; ++dir) {
+ int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, *dir);
+ if (ret >= 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<impl::MbedTlsContext *>(ctx);
+
+ // Parse certificate
+ std::string cert_str(cert);
+ int ret = mbedtls_x509_crt_parse(
+ &mctx->own_cert,
+ reinterpret_cast<const unsigned char *>(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<const unsigned char *>(password) : nullptr;
+ size_t pwd_len = password ? strlen(password) : 0;
+
+#ifdef CPPHTTPLIB_MBEDTLS_V3
+ ret = mbedtls_pk_parse_key(
+ &mctx->own_key, reinterpret_cast<const unsigned char *>(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<const unsigned char *>(key_str.c_str()),
+ key_str.size() + 1, pwd, pwd_len);
+#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<impl::MbedTlsContext *>(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;
+ }
+
+ 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<impl::MbedTlsContext *>(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<impl::MbedTlsContext *>(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;
+ }
+
+ // 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_t>(session);
+}
+
+void free_session(session_t session) {
+ if (session) { delete static_cast<impl::MbedTlsSession *>(session); }
}
-Result Client::Options(const std::string &path) {
- return cli_->Options(path);
+bool set_sni(session_t session, const char *hostname) {
+ if (!session || !hostname) { return false; }
+ auto msession = static_cast<impl::MbedTlsSession *>(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;
}
-Result Client::Options(const std::string &path, const Headers &headers) {
- return cli_->Options(path, headers);
+
+bool set_hostname(session_t session, const char *hostname) {
+ // In Mbed TLS, set_hostname also sets up hostname verification
+ return set_sni(session, hostname);
}
-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);
+TlsError connect(session_t session) {
+ TlsError err;
+ if (!session) {
+ err.code = ErrorCode::Fatal;
+ return err;
+ }
+
+ auto msession = static_cast<impl::MbedTlsSession *>(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<uint64_t>(-ret);
+ impl::mbedtls_last_error() = ret;
+ }
+
+ return err;
}
-bool Client::send(Request &req, Response &res, Error &error) {
- return cli_->send(req, res, error);
+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<impl::MbedTlsSession *>(session);
+ msession->sni_hostname = std::move(impl::mbedpending_sni());
+ impl::mbedpending_sni().clear();
+ }
+
+ return result;
}
-Result Client::send(const Request &req) { return cli_->send(req); }
+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;
+ }
-void Client::stop() { cli_->stop(); }
+ auto msession = static_cast<impl::MbedTlsSession *>(session);
-std::string Client::host() const { return cli_->host(); }
+ // Set socket to non-blocking mode
+ detail::set_nonblocking(sock, true);
+ auto cleanup =
+ detail::scope_exit([&]() { detail::set_nonblocking(sock, false); });
-int Client::port() const { return cli_->port(); }
+ 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;
+ }
+ }
-size_t Client::is_socket_open() const { return cli_->is_socket_open(); }
+ // TlsError or timeout
+ if (err) {
+ err->code = impl::map_mbedtls_error(ret, err->sys_errno);
+ err->backend_code = static_cast<uint64_t>(-ret);
+ }
+ impl::mbedtls_last_error() = ret;
+ return false;
+ }
-socket_t Client::socket() const { return cli_->socket(); }
+ if (err) { err->code = ErrorCode::Success; }
+ return true;
+}
-void
-Client::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
- cli_->set_hostname_addr_map(std::move(addr_map));
+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<impl::MbedTlsSession *>(session);
+ msession->sni_hostname = std::move(impl::mbedpending_sni());
+ impl::mbedpending_sni().clear();
+ }
+
+ return result;
}
-void Client::set_default_headers(Headers headers) {
- cli_->set_default_headers(std::move(headers));
+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<impl::MbedTlsSession *>(session);
+ int ret =
+ mbedtls_ssl_read(&msession->ssl, static_cast<unsigned char *>(buf), len);
+
+ if (ret > 0) {
+ err.code = ErrorCode::Success;
+ return static_cast<ssize_t>(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<uint64_t>(-ret);
+ impl::mbedtls_last_error() = ret;
+ return -1;
}
-void Client::set_header_writer(
- std::function<ssize_t(Stream &, Headers &)> const &writer) {
- cli_->set_header_writer(writer);
+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<impl::MbedTlsSession *>(session);
+ int ret = mbedtls_ssl_write(&msession->ssl,
+ static_cast<const unsigned char *>(buf), len);
+
+ if (ret > 0) {
+ err.code = ErrorCode::Success;
+ return static_cast<ssize_t>(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<uint64_t>(-ret);
+ impl::mbedtls_last_error() = ret;
+ return -1;
}
-void Client::set_address_family(int family) {
- cli_->set_address_family(family);
+int pending(const_session_t session) {
+ if (!session) { return 0; }
+ auto msession =
+ static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));
+ return static_cast<int>(mbedtls_ssl_get_bytes_avail(&msession->ssl));
}
-void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); }
+void shutdown(session_t session, bool graceful) {
+ if (!session) { return; }
+ auto msession = static_cast<impl::MbedTlsSession *>(session);
-void Client::set_socket_options(SocketOptions socket_options) {
- cli_->set_socket_options(std::move(socket_options));
+ 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++;
+ }
+ }
}
-void Client::set_connection_timeout(time_t sec, time_t usec) {
- cli_->set_connection_timeout(sec, usec);
+bool is_peer_closed(session_t session, socket_t sock) {
+ if (!session || sock == INVALID_SOCKET) { return true; }
+ auto msession = static_cast<impl::MbedTlsSession *>(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<impl::MbedTlsSession *>(const_cast<void *>(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<mbedtls_x509_crt *>(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<const mbedtls_x509_crt *>(cert);
+ std::string host_str(hostname);
+
+ // Check if hostname is an IP address
+ bool is_ip = impl::is_ipv4_address(host_str);
+ unsigned char ip_bytes[4];
+ if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); }
+
+ // 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) {
+ // Check if this SAN is an IPv4 address (4 bytes)
+ if (len == 4 && memcmp(p, ip_bytes, 4) == 0) { return true; }
+ // Check if this SAN is an IPv6 address (16 bytes) - skip for now
+ } 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<const char *>(p), len);
+ if (detail::match_hostname(san_name, host_str)) { return true; }
+ }
+ }
+ san = san->next;
+ }
+
+ // Fallback: Check Common Name (CN) in subject
+ 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;
}
-void Client::set_read_timeout(time_t sec, time_t usec) {
- cli_->set_read_timeout(sec, usec);
+uint64_t hostname_mismatch_code() {
+ return static_cast<uint64_t>(MBEDTLS_X509_BADCERT_CN_MISMATCH);
}
-void Client::set_write_timeout(time_t sec, time_t usec) {
- cli_->set_write_timeout(sec, usec);
+long get_verify_result(const_session_t session) {
+ if (!session) { return -1; }
+ auto msession =
+ static_cast<impl::MbedTlsSession *>(const_cast<void *>(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<long>(flags);
}
-void Client::set_basic_auth(const std::string &username,
- const std::string &password) {
- cli_->set_basic_auth(username, password);
+std::string get_cert_subject_cn(cert_t cert) {
+ if (!cert) return "";
+ auto x509 = static_cast<mbedtls_x509_crt *>(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<const char *>(name->val.p),
+ name->val.len);
+ }
+ name = name->next;
+ }
+ return "";
}
-void Client::set_bearer_token_auth(const std::string &token) {
- cli_->set_bearer_token_auth(token);
+
+std::string get_cert_issuer_name(cert_t cert) {
+ if (!cert) return "";
+ auto x509 = static_cast<mbedtls_x509_crt *>(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);
}
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-void Client::set_digest_auth(const std::string &username,
- const std::string &password) {
- cli_->set_digest_auth(username, password);
+
+bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans) {
+ sans.clear();
+ if (!cert) return false;
+ auto x509 = static_cast<mbedtls_x509_crt *>(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<const char *>(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<const char *>(p), value_len);
+ break;
+ case 6: // uniformResourceIdentifier
+ entry.type = SanType::URI;
+ entry.value =
+ std::string(reinterpret_cast<const char *>(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<mbedtls_x509_crt *>(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
+ };
-void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); }
-void Client::set_follow_location(bool on) {
- cli_->set_follow_location(on);
+ not_before = to_time_t(x509->valid_from);
+ not_after = to_time_t(x509->valid_to);
+ return true;
}
-void Client::set_path_encode(bool on) { cli_->set_path_encode(on); }
+std::string get_cert_serial(cert_t cert) {
+ if (!cert) return "";
+ auto x509 = static_cast<mbedtls_x509_crt *>(cert);
-[[deprecated("Use set_path_encode instead")]]
-void Client::set_url_encode(bool on) {
- cli_->set_path_encode(on);
+ // 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;
}
-void Client::set_compress(bool on) { cli_->set_compress(on); }
+bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {
+ if (!cert) return false;
+ auto crt = static_cast<mbedtls_x509_crt *>(cert);
+ if (!crt->raw.p || crt->raw.len == 0) return false;
+ der.assign(crt->raw.p, crt->raw.p + crt->raw.len);
+ return true;
+}
-void Client::set_decompress(bool on) { cli_->set_decompress(on); }
+const char *get_sni(const_session_t session) {
+ if (!session) return nullptr;
+ auto msession = static_cast<const impl::MbedTlsSession *>(session);
-void Client::set_interface(const std::string &intf) {
- cli_->set_interface(intf);
+ // 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;
}
-void Client::set_proxy(const std::string &host, int port) {
- cli_->set_proxy(host, port);
+uint64_t peek_error() {
+ // Mbed TLS doesn't have an error queue, return the last error
+ return static_cast<uint64_t>(-impl::mbedtls_last_error());
}
-void Client::set_proxy_basic_auth(const std::string &username,
- const std::string &password) {
- cli_->set_proxy_basic_auth(username, password);
+
+uint64_t get_error() {
+ // Mbed TLS doesn't have an error queue, return and clear the last error
+ uint64_t err = static_cast<uint64_t>(-impl::mbedtls_last_error());
+ impl::mbedtls_last_error() = 0;
+ return err;
}
-void Client::set_proxy_bearer_token_auth(const std::string &token) {
- cli_->set_proxy_bearer_token_auth(token);
+
+std::string error_string(uint64_t code) {
+ char buf[256];
+ mbedtls_strerror(-static_cast<int>(code), buf, sizeof(buf));
+ return std::string(buf);
}
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-void Client::set_proxy_digest_auth(const std::string &username,
- const std::string &password) {
- cli_->set_proxy_digest_auth(username, password);
+
+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<const unsigned char *>(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<const unsigned char *>(pem), len);
+ if (ret != 0) {
+ mbedtls_x509_crt_free(ca_chain);
+ delete ca_chain;
+ return nullptr;
+ }
+ }
+
+ return static_cast<ca_store_t>(ca_chain);
}
-#endif
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-void Client::enable_server_certificate_verification(bool enabled) {
- cli_->enable_server_certificate_verification(enabled);
+void free_ca_store(ca_store_t store) {
+ if (store) {
+ auto *ca_chain = static_cast<mbedtls_x509_crt *>(store);
+ mbedtls_x509_crt_free(ca_chain);
+ delete ca_chain;
+ }
}
-void Client::enable_server_hostname_verification(bool enabled) {
- cli_->enable_server_hostname_verification(enabled);
+bool set_ca_store(ctx_t ctx, ca_store_t store) {
+ if (!ctx || !store) { return false; }
+ auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
+ auto *ca_chain = static_cast<mbedtls_x509_crt *>(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) { return false; }
+ src = src->next;
+ }
+
+ // 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;
}
-void Client::set_server_certificate_verifier(
- std::function<SSLVerifierResponse(SSL *ssl)> verifier) {
- cli_->set_server_certificate_verifier(verifier);
+size_t get_ca_certs(ctx_t ctx, std::vector<cert_t> &certs) {
+ certs.clear();
+ if (!ctx) { return 0; }
+ auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(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<cert_t>(copy));
+ } else {
+ mbedtls_x509_crt_free(copy);
+ delete copy;
+ }
+ cert = cert->next;
+ }
+ return certs.size();
}
-#endif
-void Client::set_logger(Logger logger) {
- cli_->set_logger(std::move(logger));
+std::vector<std::string> get_ca_names(ctx_t ctx) {
+ std::vector<std::string> names;
+ if (!ctx) { return names; }
+ auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(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;
}
-void Client::set_error_logger(ErrorLogger error_logger) {
- cli_->set_error_logger(std::move(error_logger));
+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<impl::MbedTlsContext *>(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<const unsigned char *>(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<const unsigned char *>(key_pem),
+ strlen(key_pem) + 1,
+ password ? reinterpret_cast<const unsigned char *>(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<const unsigned char *>(key_pem),
+ strlen(key_pem) + 1,
+ password ? reinterpret_cast<const unsigned char *>(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;
}
-#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-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);
+bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {
+ if (!ctx || !ca_pem) { return false; }
+ auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(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<const unsigned char *>(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;
}
-void Client::set_ca_cert_store(X509_STORE *ca_cert_store) {
- if (is_ssl_) {
- static_cast<SSLClient &>(*cli_).set_ca_cert_store(ca_cert_store);
+bool set_verify_callback(ctx_t ctx, VerifyCallback callback) {
+ if (!ctx) { return false; }
+ auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
+
+ impl::get_verify_callback() = std::move(callback);
+ mbed_ctx->has_verify_callback =
+ static_cast<bool>(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 {
- cli_->set_ca_cert_store(ca_cert_store);
+ mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr);
}
+ return true;
}
-void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) {
- set_ca_cert_store(cli_->create_ca_cert_store(ca_cert, size));
+long get_verify_error(const_session_t session) {
+ if (!session) { return -1; }
+ auto *msession =
+ static_cast<impl::MbedTlsSession *>(const_cast<void *>(session));
+ return static_cast<long>(mbedtls_ssl_get_verify_result(&msession->ssl));
}
-long Client::get_openssl_verify_result() const {
- if (is_ssl_) {
- return static_cast<SSLClient &>(*cli_).get_openssl_verify_result();
+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<uint32_t>(error_code));
+ // Remove trailing newline if present
+ std::string result(buf);
+ while (!result.empty() && (result.back() == '\n' || result.back() == ' ')) {
+ result.pop_back();
}
- return -1; // NOTE: -1 doesn't match any of X509_V_ERR_???
+ return result;
}
-SSL_CTX *Client::ssl_context() const {
- if (is_ssl_) { return static_cast<SSLClient &>(*cli_).ssl_context(); }
- return nullptr;
-}
-#endif
+} // namespace tls
+
+#endif // CPPHTTPLIB_MBEDTLS_SUPPORT
} // namespace httplib