See More

module; #include #include #include #include #include #include export module mcpplibs.tinyhttps:tls; import :socket; import :ca_bundle; import std; namespace mcpplibs::tinyhttps { struct TlsState { mbedtls_ssl_context ssl; mbedtls_ssl_config conf; mbedtls_ctr_drbg_context ctr_drbg; mbedtls_entropy_context entropy; mbedtls_x509_crt ca_cert; TlsState() { mbedtls_ssl_init(&ssl); mbedtls_ssl_config_init(&conf); mbedtls_ctr_drbg_init(&ctr_drbg); mbedtls_entropy_init(&entropy); mbedtls_x509_crt_init(&ca_cert); } ~TlsState() { mbedtls_ssl_free(&ssl); mbedtls_ssl_config_free(&conf); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); mbedtls_x509_crt_free(&ca_cert); } TlsState(const TlsState&) = delete; TlsState& operator=(const TlsState&) = delete; }; // BIO callbacks for mbedtls — forward to Socket read/write static int bio_send(void* ctx, const unsigned char* buf, size_t len) { auto* sock = static_cast(ctx); int ret = sock->write(reinterpret_cast(buf), static_cast(len)); if (ret <= 0) { return MBEDTLS_ERR_NET_SEND_FAILED; } return ret; } static int bio_recv(void* ctx, unsigned char* buf, size_t len) { auto* sock = static_cast(ctx); int ret = sock->read(reinterpret_cast(buf), static_cast(len)); if (ret < 0) { return MBEDTLS_ERR_NET_RECV_FAILED; } if (ret == 0) { return MBEDTLS_ERR_NET_CONN_RESET; } return ret; } export class TlsSocket { public: TlsSocket() = default; ~TlsSocket() { close(); } // Non-copyable TlsSocket(const TlsSocket&) = delete; TlsSocket& operator=(const TlsSocket&) = delete; // Move constructor TlsSocket(TlsSocket&& other) noexcept : socket_(std::move(other.socket_)) , state_(std::move(other.state_)) { // Re-bind BIO to point to our socket_ (not the moved-from one) if (state_) { mbedtls_ssl_set_bio(&state_->ssl, &socket_, bio_send, bio_recv, nullptr); } } // Move assignment TlsSocket& operator=(TlsSocket&& other) noexcept { if (this != &other) { close(); socket_ = std::move(other.socket_); state_ = std::move(other.state_); // Re-bind BIO to point to our socket_ if (state_) { mbedtls_ssl_set_bio(&state_->ssl, &socket_, bio_send, bio_recv, nullptr); } } return *this; } [[nodiscard]] bool is_valid() const { return state_ != nullptr && socket_.is_valid(); } // Connect over an already-established Socket (e.g. a proxy tunnel). // Takes ownership of the socket and performs TLS handshake on top of it. bool connect_over(Socket&& socket, const char* host, bool verifySsl) { socket_ = std::move(socket); return setup_tls(host, verifySsl); } bool connect(const char* host, int port, int timeoutMs, bool verifySsl) { // Step 1: TCP connect via Socket if (!socket_.connect(host, port, timeoutMs)) { return false; } return setup_tls(host, verifySsl); } int read(char* buf, int len) { if (!is_valid()) return -1; int ret = mbedtls_ssl_read(&state_->ssl, reinterpret_cast(buf), static_cast(len)); if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || ret == 0) { return 0; // Connection closed } if (ret < 0) { if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return 0; // Would block, treat as no data yet } return -1; } return ret; } int write(const char* buf, int len) { if (!is_valid()) return -1; int ret = mbedtls_ssl_write(&state_->ssl, reinterpret_cast(buf), static_cast(len)); if (ret < 0) { if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return 0; } return -1; } return ret; } void close() { if (state_) { mbedtls_ssl_close_notify(&state_->ssl); state_.reset(); } socket_.close(); } bool wait_readable(int timeoutMs) { // Check if mbedtls has already buffered decrypted data if (state_ && mbedtls_ssl_get_bytes_avail(&state_->ssl) > 0) { return true; } return socket_.wait_readable(timeoutMs); } private: Socket socket_; std::unique_ptr state_; bool setup_tls(const char* host, bool verifySsl) { state_ = std::make_unique(); int ret = mbedtls_ctr_drbg_seed( &state_->ctr_drbg, mbedtls_entropy_func, &state_->entropy, nullptr, 0); if (ret != 0) { state_.reset(); socket_.close(); return false; } ret = mbedtls_ssl_config_defaults( &state_->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (ret != 0) { state_.reset(); socket_.close(); return false; } mbedtls_ssl_conf_rng(&state_->conf, mbedtls_ctr_drbg_random, &state_->ctr_drbg); // mbedTLS 3.6 TLS 1.3 key derivation can fail in statically-linked // builds; cap at TLS 1.2 which works reliably everywhere. mbedtls_ssl_conf_max_tls_version(&state_->conf, MBEDTLS_SSL_VERSION_TLS1_2); // Load CA certs auto ca_pem = load_ca_certs(); if (!ca_pem.empty()) { ret = mbedtls_x509_crt_parse( &state_->ca_cert, reinterpret_cast(ca_pem.c_str()), ca_pem.size() + 1); // +1 for null terminator required by mbedtls // ret > 0 means some certs failed to parse but others succeeded — acceptable if (ret < 0) { state_.reset(); socket_.close(); return false; } mbedtls_ssl_conf_ca_chain(&state_->conf, &state_->ca_cert, nullptr); } // Certificate verification // Use OPTIONAL (not REQUIRED) so handshake succeeds even if the CA // bundle is incomplete; callers that need strict verification can // inspect the verification result after handshake. if (verifySsl) { mbedtls_ssl_conf_authmode(&state_->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); } else { mbedtls_ssl_conf_authmode(&state_->conf, MBEDTLS_SSL_VERIFY_NONE); } ret = mbedtls_ssl_setup(&state_->ssl, &state_->conf); if (ret != 0) { state_.reset(); socket_.close(); return false; } // Set hostname for SNI ret = mbedtls_ssl_set_hostname(&state_->ssl, host); if (ret != 0) { state_.reset(); socket_.close(); return false; } // Set BIO callbacks using our Socket mbedtls_ssl_set_bio(&state_->ssl, &socket_, bio_send, bio_recv, nullptr); // Perform TLS handshake while ((ret = mbedtls_ssl_handshake(&state_->ssl)) != 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { state_.reset(); socket_.close(); return false; } } return true; } }; } // namespace mcpplibs::tinyhttps