This commit is contained in:
yhirose
2025-12-28 20:57:16 -05:00
parent f7e3bf10a2
commit 595594bea4
2 changed files with 543 additions and 8 deletions

View File

@@ -20,14 +20,14 @@
### フェーズ 1: TLS 抽象化 API の追加
- [ ] `namespace detail::tls` を宣言エリアに追加
- [ ] `ErrorCode` enum を定義
- [ ] `TlsError` 構造体を定義
- [ ] `tls_ctx_t`, `tls_session_t` 型を定義
- [ ] 関数宣言を追加
- [ ] 実装エリアに OpenSSL バックエンド実装を追加
- [ ] 既存コードは変更していない(新 API 追加のみ)
- [ ] `make test_split` 通過
- [x] `namespace detail::tls` を宣言エリアに追加
- [x] `ErrorCode` enum を定義
- [x] `TlsError` 構造体を定義
- [x] `tls_ctx_t`, `tls_session_t` 型を定義
- [x] 関数宣言を追加
- [x] 実装エリアに OpenSSL バックエンド実装を追加
- [x] 既存コードは変更していない(新 API 追加のみ)
- [x] `make test_split` 通過
### フェーズ 2: SSLClient と SSLSocketStream の移行

535
httplib.h
View File

@@ -2760,6 +2760,94 @@ bool is_field_value(const std::string &s);
} // namespace fields
// TLS abstraction layer
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
namespace tls {
// Error codes for TLS operations (backend-independent)
enum class ErrorCode : int {
Success = 0,
WantRead, // Non-blocking: need to wait for read
WantWrite, // Non-blocking: need to wait for write
PeerClosed, // Peer closed the connection
Fatal, // Unrecoverable error
SyscallError, // System call error (check sys_errno)
CertVerifyFailed, // Certificate verification failed
HostnameMismatch, // Hostname verification failed
};
// TLS error information
struct TlsError {
ErrorCode code = ErrorCode::Fatal;
uint64_t backend_code = 0; // OpenSSL: ERR_get_error(), mbedTLS: return value
int sys_errno = 0; // errno when SyscallError
};
// Opaque handles (defined as void* for abstraction)
using tls_ctx_t = void *;
using tls_session_t = void *;
using tls_cert_t = void *;
// Global initialization
bool tls_global_init();
void tls_global_cleanup();
// Client context
tls_ctx_t tls_create_client_context();
void tls_free_context(tls_ctx_t ctx);
bool tls_set_min_version(tls_ctx_t ctx, int version);
bool tls_load_ca_pem(tls_ctx_t ctx, const char *pem, size_t len);
bool tls_load_ca_file(tls_ctx_t ctx, const char *file_path);
bool tls_load_ca_dir(tls_ctx_t ctx, const char *dir_path);
bool tls_load_system_certs(tls_ctx_t ctx);
bool tls_set_client_cert_pem(tls_ctx_t ctx, const char *cert, const char *key,
const char *password);
bool tls_set_client_cert_file(tls_ctx_t ctx, const char *cert_path,
const char *key_path, const char *password);
// Server context
tls_ctx_t tls_create_server_context();
bool tls_set_server_cert_pem(tls_ctx_t ctx, const char *cert, const char *key,
const char *password);
bool tls_set_server_cert_file(tls_ctx_t ctx, const char *cert_path,
const char *key_path, const char *password);
bool tls_set_client_ca_file(tls_ctx_t ctx, const char *ca_file,
const char *ca_dir);
void tls_set_verify_client(tls_ctx_t ctx, bool require);
// Session management
tls_session_t tls_create_session(tls_ctx_t ctx, socket_t sock);
void tls_free_session(tls_session_t session);
bool tls_set_hostname(tls_session_t session, const char *hostname);
// Handshake (non-blocking capable)
TlsError tls_connect(tls_session_t session);
TlsError tls_accept(tls_session_t session);
// I/O (non-blocking capable)
ssize_t tls_read(tls_session_t session, void *buf, size_t len, TlsError &err);
ssize_t tls_write(tls_session_t session, const void *buf, size_t len,
TlsError &err);
int tls_pending(tls_session_t session);
void tls_shutdown(tls_session_t session, bool graceful);
// Connection state
bool tls_is_peer_closed(tls_session_t session);
// Certificate verification
tls_cert_t tls_get_peer_cert(tls_session_t session);
void tls_free_cert(tls_cert_t cert);
bool tls_verify_hostname(tls_cert_t cert, const char *hostname);
long tls_get_verify_result(tls_session_t session);
// Error information
uint64_t tls_peek_error();
uint64_t tls_get_error();
std::string tls_error_string(uint64_t code);
} // namespace tls
#endif
} // namespace detail
namespace stream {
@@ -12314,6 +12402,453 @@ inline void ClientImpl::set_error_logger(ErrorLogger error_logger) {
error_logger_ = std::move(error_logger);
}
/*
* TLS Abstraction Layer Implementation
*/
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
namespace detail {
namespace tls {
// Helper to map OpenSSL SSL_get_error to ErrorCode
inline 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;
}
}
inline bool tls_global_init() {
// OpenSSL 3.0+: OPENSSL_init_ssl() is called automatically
return OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS |
OPENSSL_INIT_LOAD_CRYPTO_STRINGS,
nullptr) == 1;
}
inline void tls_global_cleanup() {
// OpenSSL 3.0+: cleanup is automatic
}
inline tls_ctx_t tls_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<tls_ctx_t>(ctx);
}
inline void tls_free_context(tls_ctx_t ctx) {
if (ctx) { SSL_CTX_free(static_cast<SSL_CTX *>(ctx)); }
}
inline bool tls_set_min_version(tls_ctx_t ctx, int version) {
if (!ctx) return false;
return SSL_CTX_set_min_proto_version(static_cast<SSL_CTX *>(ctx), version) ==
1;
}
inline bool tls_load_ca_pem(tls_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;
}
inline bool tls_load_ca_file(tls_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;
}
inline bool tls_load_ca_dir(tls_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;
}
inline bool tls_load_system_certs(tls_ctx_t ctx) {
if (!ctx) return false;
auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
#ifdef _WIN32
// Windows: Load from system certificate store
auto store = SSL_CTX_get_cert_store(ssl_ctx);
if (!store) return false;
auto hStore = CertOpenSystemStoreW(NULL, L"ROOT");
if (!hStore) return false;
bool loaded_any = false;
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
}
inline bool tls_set_client_cert_pem(tls_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;
}
inline bool tls_set_client_cert_file(tls_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;
}
inline tls_ctx_t tls_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<tls_ctx_t>(ctx);
}
inline bool tls_set_server_cert_pem(tls_ctx_t ctx, const char *cert,
const char *key, const char *password) {
// Same implementation as client cert
return tls_set_client_cert_pem(ctx, cert, key, password);
}
inline bool tls_set_server_cert_file(tls_ctx_t ctx, const char *cert_path,
const char *key_path,
const char *password) {
// Same implementation as client cert file
return tls_set_client_cert_file(ctx, cert_path, key_path, password);
}
inline bool tls_set_client_ca_file(tls_ctx_t ctx, const char *ca_file,
const char *ca_dir) {
if (!ctx) return false;
auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
if (ca_file || ca_dir) {
if (SSL_CTX_load_verify_locations(ssl_ctx, ca_file, ca_dir) != 1) {
return false;
}
}
// Set CA list for client certificate request
if (ca_file) {
auto list = SSL_load_client_CA_file(ca_file);
if (list) { SSL_CTX_set_client_CA_list(ssl_ctx, list); }
}
return true;
}
inline void tls_set_verify_client(tls_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);
}
inline tls_session_t tls_create_session(tls_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<tls_session_t>(ssl);
}
inline void tls_free_session(tls_session_t session) {
if (session) { SSL_free(static_cast<SSL *>(session)); }
}
inline bool tls_set_hostname(tls_session_t session, const char *hostname) {
if (!session || !hostname) return false;
auto ssl = static_cast<SSL *>(session);
// Set SNI (Server Name Indication)
if (SSL_set_tlsext_host_name(ssl, hostname) != 1) { 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;
}
inline TlsError tls_connect(tls_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 = map_ssl_error(ssl_err, err.sys_errno);
if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); }
}
return err;
}
inline TlsError tls_accept(tls_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 = map_ssl_error(ssl_err, err.sys_errno);
if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); }
}
return err;
}
inline ssize_t tls_read(tls_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);
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 = map_ssl_error(ssl_err, err.sys_errno);
if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); }
return -1;
}
inline ssize_t tls_write(tls_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 = map_ssl_error(ssl_err, err.sys_errno);
if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); }
return -1;
}
inline int tls_pending(tls_session_t session) {
if (!session) return 0;
return SSL_pending(static_cast<SSL *>(session));
}
inline void tls_shutdown(tls_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);
}
}
}
inline bool tls_is_peer_closed(tls_session_t session) {
if (!session) return true;
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 || err == SSL_ERROR_SYSCALL;
}
inline tls_cert_t tls_get_peer_cert(tls_session_t session) {
if (!session) return nullptr;
return static_cast<tls_cert_t>(
SSL_get1_peer_certificate(static_cast<SSL *>(session)));
}
inline void tls_free_cert(tls_cert_t cert) {
if (cert) { X509_free(static_cast<X509 *>(cert)); }
}
inline bool tls_verify_hostname(tls_cert_t cert, const char *hostname) {
if (!cert || !hostname) return false;
auto x509 = static_cast<X509 *>(cert);
return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1;
}
inline long tls_get_verify_result(tls_session_t session) {
if (!session) return X509_V_ERR_UNSPECIFIED;
return SSL_get_verify_result(static_cast<SSL *>(session));
}
inline uint64_t tls_peek_error() { return ERR_peek_last_error(); }
inline uint64_t tls_get_error() { return ERR_get_error(); }
inline std::string tls_error_string(uint64_t code) {
char buf[256];
ERR_error_string_n(static_cast<unsigned long>(code), buf, sizeof(buf));
return std::string(buf);
}
} // namespace tls
} // namespace detail
#endif
/*
* SSL Implementation
*/