diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 559179acd9..16013da492 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -321,7 +321,7 @@ jobs: if: runner.os == 'Linux' && matrix.extra != 'android' && matrix.extra != 'loongarch64' run: | sudo apt-get update -y -qq - sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev + sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev libcurl4-openssl-dev - name: Install macOS SDL3 dependencies if: runner.os == 'macOS' && matrix.id == 'macos' @@ -475,7 +475,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update -y -qq - sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev + sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev libcurl4-openssl-dev - name: Install macOS dependencies if: runner.os == 'macOS' @@ -524,7 +524,7 @@ jobs: steps: - name: Install Linux dependencies (Alpine) run: | - apk add build-base wget git bash cmake python3 glu-dev sdl3-dev sdl3_ttf-dev + apk add build-base wget git bash cmake python3 glu-dev sdl3-dev sdl3_ttf-dev curl-dev - uses: actions/checkout@v7 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 68e775c8e2..30ee0012ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,7 +271,20 @@ if(ANDROID) set(USING_GLES2 ON) endif() -if(NOT ANDROID AND NOT WIN32 AND (NOT APPLE OR IOS)) +# naett wraps a native HTTPS implementation per platform: WinHTTP on Windows, the Java +# APIs on Android, NSURLSession on macOS, and libcurl elsewhere. +if(WIN32 OR ANDROID OR (APPLE AND NOT IOS)) + # Nothing to do, these need no external library. +elseif(UNIX AND NOT APPLE AND NOT LIBRETRO) + # We only need the curl headers at build time - libcurl itself is loaded with dlopen + # (see ext/naett-lib/src/naett_curl.h), so it stays optional at runtime and a build made + # here still starts on a machine without it, just without HTTPS. + find_package(CURL) + if(NOT CURL_FOUND) + message(STATUS "libcurl headers not found - building without HTTPS support") + set(HTTPS_NOT_AVAILABLE ON) + endif() +else() set(HTTPS_NOT_AVAILABLE ON) endif() diff --git a/Common/Net/Resolve.cpp b/Common/Net/Resolve.cpp index 43538fb298..bc52b912ac 100644 --- a/Common/Net/Resolve.cpp +++ b/Common/Net/Resolve.cpp @@ -16,6 +16,12 @@ #ifndef HTTPS_NOT_AVAILABLE #include "ext/naett-lib/naett.h" +// Note: PPSSPP_PLATFORM(LINUX) is also set on Android, which needs no loader. +#if PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID) +// On Linux, naett goes through libcurl, which we load at runtime - so HTTPS support is +// only known once we've tried. +#include "ext/naett-lib/src/naett_curl.h" +#endif #endif #if PPSSPP_PLATFORM(ANDROID) @@ -44,13 +50,25 @@ void Init() { _assert_(gJvm != nullptr); naettInit(gJvm); #else - naettInit(NULL); + if (HTTPSAvailable()) { + naettInit(NULL); + } #endif #endif g_naettInitialized = true; } } +bool HTTPSAvailable() { +#ifdef HTTPS_NOT_AVAILABLE + return false; +#elif PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID) + return naettCurlLoad() != 0; +#else + return true; +#endif +} + void Shutdown() { #ifdef _WIN32 if (g_wsaInitialized) { diff --git a/Common/Net/Resolve.h b/Common/Net/Resolve.h index c6f27dfcc9..4ede3d4be2 100644 --- a/Common/Net/Resolve.h +++ b/Common/Net/Resolve.h @@ -12,6 +12,11 @@ namespace net { void Init(); void Shutdown(); +// Whether we have a working HTTPS backend. False if PPSSPP was built without one, and on +// Linux also if libcurl couldn't be loaded at runtime. Safe to call before Init(). +// Platform ports should factor this into SYSPROP_SUPPORTS_HTTPS. +bool HTTPSAvailable(); + enum class DNSType { ANY = 0, IPV4 = 1, diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index 28a9ec26dd..940e31214e 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -1193,7 +1193,8 @@ bool System_GetPropertyBool(SystemProperty prop) { return true; // FileUtil.cpp: OpenFileInEditor #ifndef HTTPS_NOT_AVAILABLE case SYSPROP_SUPPORTS_HTTPS: - return !g_Config.bDisableHTTPS; + // On Linux this also depends on whether libcurl could be loaded. + return !g_Config.bDisableHTTPS && net::HTTPSAvailable(); #endif case SYSPROP_HAS_FOLDER_BROWSER: case SYSPROP_HAS_FILE_BROWSER: diff --git a/ext/naett-build/CMakeLists.txt b/ext/naett-build/CMakeLists.txt index bd3934e0b3..526dd51e5f 100644 --- a/ext/naett-build/CMakeLists.txt +++ b/ext/naett-build/CMakeLists.txt @@ -14,10 +14,14 @@ elseif(ANDROID) elseif(APPLE) list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_osx.c) elseif(UNIX) - list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_linux.c) + list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_linux.c ${SRC_DIR}/naett_curl.c) endif() add_library(naett STATIC ${ALL_SOURCE_FILES}) if(WIN32) target_link_libraries(naett winhttp) +elseif(UNIX AND NOT APPLE AND NOT ANDROID) + # Headers only - libcurl itself is dlopen'd, see ../naett/src/naett_curl.h. + target_include_directories(naett PRIVATE ${CURL_INCLUDE_DIRS}) + target_link_libraries(naett ${CMAKE_DL_LIBS}) endif() diff --git a/ext/naett-lib/README-ppsspp.md b/ext/naett-lib/README-ppsspp.md index 3a81953acd..114b630b61 100644 --- a/ext/naett-lib/README-ppsspp.md +++ b/ext/naett-lib/README-ppsspp.md @@ -21,3 +21,15 @@ Keep this list up to date - it's what makes it possible to move to a newer upstr `exit`/`calloc`/`realloc`/`free`/`fprintf` but never included either header - in the amalgam it got them from `naett_core.c` further up the concatenation. Building it on its own is an error with a modern compiler. +- `src/naett_curl.h` / `src/naett_curl.c`: new, ours. Loads libcurl with `dlopen` instead of + linking against it, so libcurl stays a soft dependency at runtime. `naett_linux.c` includes + that header in place of ``. +- `src/naett_linux.c`: replaced the `panic()` that called `exit(1)` on a pipe or + `curl_multi_perform` failure. Taking the whole emulator down because a download failed isn't + acceptable, so the backend now disables itself and requests complete with `naettGenericError`. +- `src/naett_linux.c`: `CURLINFO_RESPONSE_CODE` writes a `long`, and `res->code` is an `int` - + upstream passed `&res->code` straight to `curl_easy_getinfo`, writing 8 bytes into 4. Reads + into a `long` local now. +- `src/naett_linux.c`: `curl_easy_setopt` is varargs and takes a `long` for these options; + upstream passed `int` literals and `int` variables, which is UB on LP64 (and what curl's own + typecheck macros warn about). They're `1L`/`(long)` now. diff --git a/ext/naett-lib/src/naett_curl.c b/ext/naett-lib/src/naett_curl.c new file mode 100644 index 0000000000..4a9a7617aa --- /dev/null +++ b/ext/naett-lib/src/naett_curl.c @@ -0,0 +1,78 @@ +// PPSSPP addition - not part of upstream naett. See naett_curl.h. + +#if __linux__ && !__ANDROID__ + +#define NAETT_CURL_INTERNAL +#include "naett_curl.h" + +#include +#include + +NaettCurl g_curl; + +// libcurl's SONAME has been libcurl.so.4 since 2007. The gnutls and nss flavors are +// separate sonames on Debian-likes and old Fedora respectively, and are ABI compatible. +// libcurl.so last, since that one only exists if the -dev package is installed. +static const char *g_curlLibNames[] = { + "libcurl.so.4", + "libcurl-gnutls.so.4", + "libcurl-nss.so.4", + "libcurl.so", +}; + +static int LoadSymbols(void *lib) { + // Every one of these has existed since libcurl 7.28 (2012), so a partial load means + // something is badly wrong rather than "the host libcurl is a bit old" - bail out + // entirely rather than crash later on a null pointer. +#define LOAD(field, name) \ + g_curl.field = (__typeof__(g_curl.field))dlsym(lib, name); \ + if (!g_curl.field) { \ + fprintf(stderr, "naett: libcurl is missing %s\n", name); \ + return 0; \ + } + + LOAD(global_init, "curl_global_init") + LOAD(easy_init, "curl_easy_init") + LOAD(easy_setopt, "curl_easy_setopt") + LOAD(easy_getinfo, "curl_easy_getinfo") + LOAD(easy_cleanup, "curl_easy_cleanup") + LOAD(multi_init, "curl_multi_init") + LOAD(multi_add_handle, "curl_multi_add_handle") + LOAD(multi_perform, "curl_multi_perform") + LOAD(multi_info_read, "curl_multi_info_read") + LOAD(multi_wait, "curl_multi_wait") + LOAD(slist_append, "curl_slist_append") + LOAD(slist_free_all, "curl_slist_free_all") + +#undef LOAD + return 1; +} + +int naettCurlLoad(void) { + static int loaded = -1; + if (loaded >= 0) { + return loaded; + } + + loaded = 0; + for (size_t i = 0; i < sizeof(g_curlLibNames) / sizeof(g_curlLibNames[0]); i++) { + // RTLD_GLOBAL so that libcurl's own dependencies resolve normally. + void *lib = dlopen(g_curlLibNames[i], RTLD_NOW | RTLD_GLOBAL); + if (!lib) { + continue; + } + if (LoadSymbols(lib)) { + loaded = 1; + } else { + dlclose(lib); + } + break; + } + + if (!loaded) { + fprintf(stderr, "naett: libcurl not found, HTTPS will be unavailable\n"); + } + return loaded; +} + +#endif diff --git a/ext/naett-lib/src/naett_curl.h b/ext/naett-lib/src/naett_curl.h new file mode 100644 index 0000000000..b474027525 --- /dev/null +++ b/ext/naett-lib/src/naett_curl.h @@ -0,0 +1,76 @@ +// PPSSPP addition - not part of upstream naett. +// +// Loads libcurl at runtime instead of linking against it, so that libcurl stays a soft +// dependency: a build made on a machine with the curl headers still starts on a machine +// without libcurl installed, just with HTTPS reported as unavailable. Same idea as +// Common/GPU/Vulkan/VulkanLoader.cpp. +// +// The curl headers are still needed at build time, for the types and the option enums. +// Only naett_linux.c and naett_curl.c want the redirect macros, so those are behind +// NAETT_CURL_INTERNAL; everyone else gets just the loader entry point and doesn't pull +// into their translation unit. + +#ifndef NAETT_CURL_H +#define NAETT_CURL_H + +#ifdef __cplusplus +extern "C" { +#endif + +// Loads libcurl if it isn't loaded yet, and returns 1 if it's available. +// The result is cached, so calling this repeatedly is cheap. Not thread safe - call it +// once during startup before any other thread can reach it (PPSSPP does so in net::Init). +int naettCurlLoad(void); + +#ifdef __cplusplus +} +#endif + +#ifdef NAETT_CURL_INTERNAL + +#include + +// curl.h defines these as typechecking macros when built with GCC/Clang. We need the +// plain names to redirect, and we lose the typechecking, which is fine - the calls below +// are checked at build time in exactly the same way for anyone building against a normal +// libcurl. +#undef curl_easy_setopt +#undef curl_easy_getinfo + +typedef struct { + CURLcode (*global_init)(long flags); + + CURL *(*easy_init)(void); + CURLcode (*easy_setopt)(CURL *handle, CURLoption option, ...); + CURLcode (*easy_getinfo)(CURL *handle, CURLINFO info, ...); + void (*easy_cleanup)(CURL *handle); + + CURLM *(*multi_init)(void); + CURLMcode (*multi_add_handle)(CURLM *multi, CURL *easy); + CURLMcode (*multi_perform)(CURLM *multi, int *runningHandles); + CURLMsg *(*multi_info_read)(CURLM *multi, int *msgsInQueue); + CURLMcode (*multi_wait)(CURLM *multi, struct curl_waitfd extraFDs[], unsigned int extraNFDs, int timeoutMS, + int *numFDs); + + struct curl_slist *(*slist_append)(struct curl_slist *list, const char *data); + void (*slist_free_all)(struct curl_slist *list); +} NaettCurl; + +extern NaettCurl g_curl; + +#define curl_global_init g_curl.global_init +#define curl_easy_init g_curl.easy_init +#define curl_easy_setopt g_curl.easy_setopt +#define curl_easy_getinfo g_curl.easy_getinfo +#define curl_easy_cleanup g_curl.easy_cleanup +#define curl_multi_init g_curl.multi_init +#define curl_multi_add_handle g_curl.multi_add_handle +#define curl_multi_perform g_curl.multi_perform +#define curl_multi_info_read g_curl.multi_info_read +#define curl_multi_wait g_curl.multi_wait +#define curl_slist_append g_curl.slist_append +#define curl_slist_free_all g_curl.slist_free_all + +#endif // NAETT_CURL_INTERNAL + +#endif // NAETT_CURL_H diff --git a/ext/naett-lib/src/naett_linux.c b/ext/naett-lib/src/naett_linux.c index 0b7572592a..165abb1960 100644 --- a/ext/naett-lib/src/naett_linux.c +++ b/ext/naett-lib/src/naett_linux.c @@ -2,7 +2,11 @@ #if __linux__ && !__ANDROID__ -#include +// PPSSPP: libcurl is dlopen'd rather than linked, see naett_curl.h. This header stands in +// for and redirects the curl_* calls below through function pointers. +#define NAETT_CURL_INTERNAL +#include "naett_curl.h" + #include #include #include @@ -15,9 +19,14 @@ static pthread_t workerThread; static int handleReadFD = 0; static int handleWriteFD = 0; -static void panic(const char* message) { - fprintf(stderr, "%s\n", message); - exit(1); +// PPSSPP: upstream had a panic() here that called exit(1). Taking the whole application +// down because a download failed isn't acceptable for us, so failures now leave the +// backend disabled and requests complete with naettGenericError instead. +static int workerRunning = 0; + +static void fail(const char* message) { + fprintf(stderr, "naett: %s\n", message); + workerRunning = 0; } static void* curlWorker(void* data) { @@ -34,10 +43,11 @@ static void* curlWorker(void* data) { int newHandlePos = 0; - while (1) { + while (workerRunning) { int status = curl_multi_perform(mc, &activeHandles); if (status != CURLM_OK) { - panic("CURL processing failure"); + fail("curl_multi_perform failed, shutting down the HTTP worker"); + break; } struct CURLMsg* message = curl_multi_info_read(mc, &messagesLeft); @@ -45,7 +55,12 @@ static void* curlWorker(void* data) { CURL* handle = message->easy_handle; InternalResponse* res = NULL; curl_easy_getinfo(handle, CURLINFO_PRIVATE, (char**)&res); - curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &res->code); + // PPSSPP: CURLINFO_RESPONSE_CODE writes a long, and res->code is an int - + // upstream passed &res->code directly, which writes 8 bytes into 4 and only + // gets away with it because the next field happens to absorb the zeroes. + long responseCode = 0; + curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &responseCode); + res->code = (int)responseCode; res->complete = 1; curl_easy_cleanup(handle); } @@ -72,11 +87,19 @@ static void* curlWorker(void* data) { } void naettPlatformInit(naettInitData initData) { + if (!naettCurlLoad()) { + return; + } curl_global_init(CURL_GLOBAL_ALL); CURLM* mc = curl_multi_init(); + if (!mc) { + fail("curl_multi_init failed"); + return; + } int fds[2]; if (pipe(fds) != 0) { - panic("Failed to open pipe"); + fail("failed to open pipe"); + return; } handleReadFD = fds[0]; handleWriteFD = fds[1]; @@ -87,7 +110,10 @@ void naettPlatformInit(naettInitData initData) { pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - pthread_create(&workerThread, &attr, curlWorker, mc); + workerRunning = 1; + if (pthread_create(&workerThread, &attr, curlWorker, mc) != 0) { + fail("failed to start the HTTP worker thread"); + } } int naettPlatformInitRequest(InternalRequest* req) { @@ -121,19 +147,19 @@ static void setupMethod(CURL* curl, const char* method) { case METHOD('G', 'E', 'T'): case METHOD('C', 'O', 'N'): case METHOD('O', 'P', 'T'): - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); break; case METHOD('P', 'O', 'S'): case METHOD('P', 'A', 'T'): case METHOD('D', 'E', 'L'): - curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_POST, 1L); break; case METHOD('P', 'U', 'T'): - curl_easy_setopt(curl, CURLOPT_UPLOAD, 1); + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); break; case METHOD('H', 'E', 'A'): case METHOD('T', 'R', 'A'): - curl_easy_setopt(curl, CURLOPT_NOBODY, 1); + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); break; } @@ -177,9 +203,17 @@ static size_t headerCallback(char* buffer, size_t size, size_t nitems, void* use void naettPlatformMakeRequest(InternalResponse* res) { InternalRequest* req = res->request; + if (!workerRunning) { + // No libcurl, or the worker died. Complete the request as failed rather than + // leaving the caller waiting forever on a request nobody is going to run. + res->code = naettGenericError; + res->complete = 1; + return; + } + CURL* c = curl_easy_init(); curl_easy_setopt(c, CURLOPT_URL, req->url); - curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, req->options.timeoutMS); + curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, (long)req->options.timeoutMS); curl_easy_setopt(c, CURLOPT_READFUNCTION, readCallback); curl_easy_setopt(c, CURLOPT_READDATA, res); @@ -190,10 +224,10 @@ void naettPlatformMakeRequest(InternalResponse* res) { curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, headerCallback); curl_easy_setopt(c, CURLOPT_HEADERDATA, res); - curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); int bodySize = res->request->options.bodyReader(NULL, 0, res->request->options.bodyReaderData); - curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, bodySize); + curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)bodySize); setupMethod(c, req->options.method); @@ -228,6 +262,10 @@ void naettPlatformFreeRequest(InternalRequest* req) { } void naettPlatformCloseResponse(InternalResponse* res) { + if (!naettCurlLoad()) { + // Nothing was ever allocated by curl, and the function pointers are all null. + return; + } curl_slist_free_all(res->headerList); }