diff --git a/engine/common/http/net_http_tls.c b/engine/common/http/net_http_tls.c new file mode 100644 index 00000000..f93227d4 --- /dev/null +++ b/engine/common/http/net_http_tls.c @@ -0,0 +1,277 @@ +/* +net_http_tls.c - TLS backend for the built-in HTTP client (mbedTLS) +Copyright (C) 2026 Xash3D FWGS contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "common.h" +#include "net_ws_private.h" +#include "net_http_tls.h" + +#if XASH_MBEDTLS + +#include +#include +#include +#include +#include + +extern poolhandle_t http_mempool; + +static CVAR_DEFINE_AUTO( http_tls_cafile, "cacert.pem", FCVAR_PRIVILEGED, "path to CA bundle (PEM) used to verify HTTPS servers" ); +static CVAR_DEFINE_AUTO( http_tls_insecure, "0", FCVAR_PRIVILEGED, "skip HTTPS certificate verification (debug only)" ); +static CVAR_DEFINE_AUTO( http_tls_verbose, "0", FCVAR_PRIVILEGED, "mbedTLS debug verbosity (0=off, 1=error, 2=state, 3=info, 4=trace)" ); + +// FIXME: implement certificate pinning +typedef int (*pin_verify_fn_t)( void *cert, int depth, uint32_t *flags ); + +static struct +{ + qboolean inited; + qboolean has_ca; + mbedtls_x509_crt cacert; + pin_verify_fn_t pin_verify; // reserved +} g_tls; + +struct tlsctx_s +{ + mbedtls_ssl_context ssl; + mbedtls_ssl_config conf; + int socket; // not owned; caller closes +}; + +static void HTTP_TlsLogDebug( void *ctx, int level, const char *file, int line, const char *str ) +{ + Con_Printf( "TLS[%d] %s:%d %s", level, COM_FileWithoutPath( file ), line, str ); +} + +static void HTTP_TlsLogErr( const char *what, int ret ) +{ + char msg[128]; + mbedtls_strerror( ret, msg, sizeof( msg )); + Con_Printf( S_ERROR "TLS %s failed: %s (-0x%04x)\n", what, msg, (unsigned int)-ret ); +} + +static int HTTP_TlsBioSend( void *ctx, const unsigned char *buf, size_t len ) +{ + int fd = *(int *)ctx; + int n = send( fd, buf, len, 0 ); + + if( n >= 0 ) + return n; + + int err = WSAGetLastError(); + if( err == WSAEWOULDBLOCK || err == WSAEINPROGRESS ) + return MBEDTLS_ERR_SSL_WANT_WRITE; + + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; +} + +static int HTTP_TlsBioRecv( void *ctx, unsigned char *buf, size_t len ) +{ + int fd = *(int *)ctx; + int n = recv( fd, buf, len, 0 ); + + if( n > 0 ) + return n; + + if( n == 0 ) + return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; + + int err = WSAGetLastError(); + if( err == WSAEWOULDBLOCK || err == WSAEINPROGRESS ) + return MBEDTLS_ERR_SSL_WANT_READ; + + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; +} + +static qboolean HTTP_TlsLoadCA( void ) +{ + const char *path = http_tls_cafile.string; + fs_offset_t len = 0; + byte *data = FS_LoadFile( path, &len, false ); + + if( !data || len <= 0 ) + { + Con_Printf( S_WARN "TLS: CA bundle '%s' not found; HTTPS verification will fail\n", path ); + Mem_Free( data ); + return false; + } + + int ret = mbedtls_x509_crt_parse( &g_tls.cacert, data, len + 1 ); + Mem_Free( data ); + + if( ret < 0 ) + { + HTTP_TlsLogErr( "x509_crt_parse", ret ); + return false; + } + + if( ret > 0 ) + Con_Reportf( S_WARN "TLS: %d certificate(s) in '%s' failed to parse\n", ret, path ); + + return true; +} + +void HTTP_TlsInit( void ) +{ + if( g_tls.inited ) + return; + + Cvar_RegisterVariable( &http_tls_cafile ); + Cvar_RegisterVariable( &http_tls_insecure ); + Cvar_RegisterVariable( &http_tls_verbose ); + + psa_status_t pstatus = psa_crypto_init(); + if( pstatus != PSA_SUCCESS ) + { + Con_Printf( S_ERROR "TLS psa_crypto_init failed (status %d)\n", (int)pstatus ); + return; + } + + mbedtls_x509_crt_init( &g_tls.cacert ); + g_tls.has_ca = HTTP_TlsLoadCA(); + g_tls.pin_verify = NULL; + g_tls.inited = true; +} + +void HTTP_TlsShutdown( void ) +{ + if( !g_tls.inited ) + return; + + mbedtls_x509_crt_free( &g_tls.cacert ); + mbedtls_psa_crypto_free(); + g_tls.inited = false; + g_tls.has_ca = false; +} + +qboolean HTTP_TlsAvailable( void ) +{ + return g_tls.inited; +} + +tlsctx_t *HTTP_TlsNew( int socket, const char *hostname ) +{ + if( !g_tls.inited ) + return NULL; + + tlsctx_t *ctx = Mem_Calloc( http_mempool, sizeof( *ctx )); + ctx->socket = socket; + + mbedtls_ssl_init( &ctx->ssl ); + mbedtls_ssl_config_init( &ctx->conf ); + + int ret = mbedtls_ssl_config_defaults( &ctx->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT ); + if( ret != 0 ) + { + HTTP_TlsLogErr( "ssl_config_defaults", ret ); + HTTP_TlsFree( ctx ); + return NULL; + } + + int authmode = MBEDTLS_SSL_VERIFY_REQUIRED; + if( http_tls_insecure.value != 0 || !g_tls.has_ca ) + { + if( !g_tls.has_ca ) + Con_Printf( S_WARN "TLS: no CA bundle loaded, peer cert will not be verified\n" ); + else + Con_Printf( S_WARN "TLS: http_tls_insecure is set, peer cert will not be verified\n" ); + + authmode = MBEDTLS_SSL_VERIFY_NONE; + } + + mbedtls_ssl_conf_authmode( &ctx->conf, authmode ); + mbedtls_ssl_conf_ca_chain( &ctx->conf, &g_tls.cacert, NULL ); + mbedtls_ssl_conf_dbg( &ctx->conf, HTTP_TlsLogDebug, NULL ); + mbedtls_debug_set_threshold( http_tls_verbose.value ); + + ret = mbedtls_ssl_setup( &ctx->ssl, &ctx->conf ); + if( ret != 0 ) + { + HTTP_TlsLogErr( "ssl_setup", ret ); + HTTP_TlsFree( ctx ); + return NULL; + } + + if( hostname && *hostname ) + { + ret = mbedtls_ssl_set_hostname( &ctx->ssl, hostname ); + if( ret != 0 ) + { + HTTP_TlsLogErr( "ssl_set_hostname", ret ); + HTTP_TlsFree( ctx ); + return NULL; + } + } + + mbedtls_ssl_set_bio( &ctx->ssl, &ctx->socket, HTTP_TlsBioSend, HTTP_TlsBioRecv, NULL ); + + return ctx; +} + +void HTTP_TlsFree( tlsctx_t *ctx ) +{ + if( !ctx ) + return; + + mbedtls_ssl_free( &ctx->ssl ); + mbedtls_ssl_config_free( &ctx->conf ); + Mem_Free( ctx ); +} + +int HTTP_TlsHandshake( tlsctx_t *ctx ) +{ + int ret = mbedtls_ssl_handshake( &ctx->ssl ); + + if( ret == 0 ) + return HTTP_TLS_OK; + + if( ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE ) + return HTTP_TLS_WANT; + + HTTP_TlsLogErr( "handshake", ret ); + return HTTP_TLS_ERROR; +} + +int HTTP_TlsSend( tlsctx_t *ctx, const void *buf, int len ) +{ + int ret = mbedtls_ssl_write( &ctx->ssl, (const unsigned char *)buf, (size_t)len ); + + if( ret >= 0 ) + return ret; + + if( ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE ) + return HTTP_TLS_WANT; + + HTTP_TlsLogErr( "send", ret ); + return HTTP_TLS_ERROR; +} + +int HTTP_TlsRecv( tlsctx_t *ctx, void *buf, int len ) +{ + int ret = mbedtls_ssl_read( &ctx->ssl, (unsigned char *)buf, (size_t)len ); + + if( ret > 0 ) + return ret; + + if( ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY ) + return 0; + + if( ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE ) + return HTTP_TLS_WANT; + + HTTP_TlsLogErr( "recv", ret ); + return HTTP_TLS_ERROR; +} + +#endif // XASH_MBEDTLS diff --git a/engine/common/http/net_http_tls.h b/engine/common/http/net_http_tls.h index 7f6b6bad..3b34ce1a 100644 --- a/engine/common/http/net_http_tls.h +++ b/engine/common/http/net_http_tls.h @@ -17,10 +17,37 @@ GNU General Public License for more details. #include "xash3d_types.h" -// No TLS for you! Yet. -static inline qboolean HTTP_TlsAvailable( void ) +typedef struct tlsctx_s tlsctx_t; + +enum { - return false; -} + HTTP_TLS_OK = 0, // handshake/op completed + HTTP_TLS_WANT = -1, // would block, retry next frame + HTTP_TLS_ERROR = -2 // permanent failure, tear the connection down +}; + +#if XASH_MBEDTLS + +void HTTP_TlsInit( void ); +void HTTP_TlsShutdown( void ); +qboolean HTTP_TlsAvailable( void ); +tlsctx_t *HTTP_TlsNew( int socket, const char *hostname ); +void HTTP_TlsFree( tlsctx_t *ctx ); +int HTTP_TlsHandshake( tlsctx_t *ctx ); +int HTTP_TlsSend( tlsctx_t *ctx, const void *buf, int len ); +int HTTP_TlsRecv( tlsctx_t *ctx, void *buf, int len ); + +#else // !XASH_MBEDTLS + +static inline void HTTP_TlsInit( void ) { } +static inline void HTTP_TlsShutdown( void ) { } +static inline qboolean HTTP_TlsAvailable( void ) { return false; } +static inline tlsctx_t *HTTP_TlsNew( int socket, const char *hostname ) { return NULL; } +static inline void HTTP_TlsFree( tlsctx_t *ctx ) { } +static inline int HTTP_TlsHandshake( tlsctx_t *ctx ) { return HTTP_TLS_ERROR; } +static inline int HTTP_TlsSend( tlsctx_t *ctx, const void *buf, int len ) { return HTTP_TLS_ERROR; } +static inline int HTTP_TlsRecv( tlsctx_t *ctx, void *buf, int len ) { return HTTP_TLS_ERROR; } + +#endif // XASH_MBEDTLS #endif // NET_HTTP_TLS_H diff --git a/engine/common/http/net_http_xash.c b/engine/common/http/net_http_xash.c index 2304e541..95de2a0b 100644 --- a/engine/common/http/net_http_xash.c +++ b/engine/common/http/net_http_xash.c @@ -99,6 +99,7 @@ static struct http_static_s qboolean resolving; } http; +poolhandle_t http_mempool; static CVAR_DEFINE_AUTO( http_useragent, "", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "User-Agent string" ); static CVAR_DEFINE_AUTO( http_autoremove, "1", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "remove broken files" ); @@ -1449,6 +1450,7 @@ HTTP_Init void HTTP_Init( void ) { http.first_file = NULL; + http_mempool = Mem_AllocPool( "HTTP" ); Cmd_AddRestrictedCommand( "http_download", HTTP_Download_f, "add file to download queue" ); Cmd_AddRestrictedCommand( "http_skip", HTTP_Skip_f, "skip current download server" ); @@ -1481,4 +1483,6 @@ void HTTP_Shutdown( void ) http.first_server = http.first_server->next; Mem_Free( tmp ); } + + Mem_FreePool( &http_mempool ); }