From e5b6cbf09163cf80f0d136aa22bcb770feb281ac Mon Sep 17 00:00:00 2001 From: SoftFever <103989404+SoftFever@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:06:14 +0800 Subject: [PATCH] Let plugins make HTTPS requests on macOS (#16638) * Let the bundled Python verify HTTPS certificates on macOS * Restore SSL_CERT_FILE after the bundled Python override test --- deps/OpenSSL/OpenSSL.cmake | 24 +++++++---- deps/python3/python3.cmake | 9 ++-- tests/slic3rutils/test_orca_cloud_agent.cpp | 25 ----------- tests/slic3rutils/test_plugin_lifecycle.cpp | 46 +++++++++++++++++++++ tests/test_utils.hpp | 26 ++++++++++++ 5 files changed, 94 insertions(+), 36 deletions(-) diff --git a/deps/OpenSSL/OpenSSL.cmake b/deps/OpenSSL/OpenSSL.cmake index 35b17ed50e..971e2f1ad8 100644 --- a/deps/OpenSSL/OpenSSL.cmake +++ b/deps/OpenSSL/OpenSSL.cmake @@ -16,6 +16,8 @@ else() endif() endif() +set(_openssl_dir ${DESTDIR}) + if(WIN32) set(_openssl_msvc_env CC=cl CXX=cl RC=rc CL=/FS) # OpenSSL's perl Configure honors the CC environment variable, but the @@ -43,6 +45,14 @@ if(WIN32) else() if(APPLE) set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}) + # OPENSSLDIR is only where OpenSSL looks at runtime for cert.pem, certs/ + # and openssl.cnf; install_sw installs nothing there. Point it at the + # system CA bundle, the one libcurl already uses here, so the bundled + # Python's ssl module can verify HTTPS. Elsewhere it stays the prefix: + # Windows Python reads the system certificate store, and on Linux, where + # the bundle's location differs by distro, CurlGlobalInit points + # SSL_CERT_FILE at it at startup. + set(_openssl_dir /etc/ssl) else() # A static library that is embedded into a shared object must not export # its symbols. On Linux the running process also loads the system OpenSSL @@ -81,7 +91,7 @@ ExternalProject_Add(dep_OpenSSL # URL_HASH SHA256=8c776993154652d0bb393f506d850b811517c8bd8d24b1008aef57fbe55d3f31 DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/OpenSSL CONFIGURE_COMMAND ${_conf_cmd} ${_cross_arch} - "--openssldir=${DESTDIR}" + "--openssldir=${_openssl_dir}" "--prefix=${DESTDIR}" # OpenSSL's linux-x86_64 target sets multilib=64, so it installs to # /lib64 while every other dep uses /lib. CPython's @@ -115,12 +125,12 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" ) -if (NOT WIN32 AND NOT APPLE) - # OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for - # example to add -fvisibility=hidden) relinks the archives from stale - # objects instead of recompiling them, and the change silently has no - # effect. Drop the objects whenever this recipe changes so the next build - # actually recompiles them. +if (NOT WIN32) + # OpenSSL's object rules do not depend on the configured flags or paths, so + # reconfiguring it (a new compiler flag, a different --openssldir) relinks + # the archives from stale objects instead of recompiling them, and the + # change silently has no effect. Drop the objects whenever this recipe + # changes so the next build actually recompiles them. ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR) ExternalProject_Add_Step(dep_OpenSSL clean_objects DEPENDEES configure diff --git a/deps/python3/python3.cmake b/deps/python3/python3.cmake index f520c536d5..4f60bb1b14 100644 --- a/deps/python3/python3.cmake +++ b/deps/python3/python3.cmake @@ -365,10 +365,11 @@ if(TARGET dep_ZLIB) add_dependencies(dep_python3 dep_ZLIB) endif() -if (NOT WIN32 AND NOT APPLE) - # CPython's Makefile rules for _ssl and _hashlib depend only on their own - # sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make - # them relink and they keep the previous symbols. On an incremental tree, +if (NOT WIN32) + # _ssl and _hashlib embed the static OpenSSL built in deps/ (Windows uses + # CPython's prebuilt OpenSSL instead). Their Makefile rules depend only on + # their own sources, not on the OpenSSL archives, so a rebuilt OpenSSL does + # not make them relink and they keep the previous one. On an incremental tree, # drop the built modules and relink them against the current OpenSSL; a # fresh build is left alone (its PGO target builds them). "make" alone is a # no-op once PGO has run, so sharedmods is invoked explicitly. diff --git a/tests/slic3rutils/test_orca_cloud_agent.cpp b/tests/slic3rutils/test_orca_cloud_agent.cpp index 99c4180a9a..485b662c0b 100644 --- a/tests/slic3rutils/test_orca_cloud_agent.cpp +++ b/tests/slic3rutils/test_orca_cloud_agent.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -23,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -196,29 +194,6 @@ private: std::vector> m_connections; }; -// Sets an environment variable for the lifetime of the guard and restores the previous value. -class ScopedEnvVar -{ -public: - ScopedEnvVar(std::string name, const std::string& value) : m_name(std::move(name)) - { - if (const char* old = boost::nowide::getenv(m_name.c_str())) - m_previous = old; - boost::nowide::setenv(m_name.c_str(), value.c_str(), 1); - } - ~ScopedEnvVar() - { - if (m_previous) - boost::nowide::setenv(m_name.c_str(), m_previous->c_str(), 1); - else - boost::nowide::unsetenv(m_name.c_str()); - } - -private: - std::string m_name; - std::optional m_previous; -}; - // What the agent reported through its callbacks, recorded on whichever thread delivers it so the // assertions can run on the test thread. struct CallbackLog diff --git a/tests/slic3rutils/test_plugin_lifecycle.cpp b/tests/slic3rutils/test_plugin_lifecycle.cpp index 498935c2f6..0a1a94dcef 100644 --- a/tests/slic3rutils/test_plugin_lifecycle.cpp +++ b/tests/slic3rutils/test_plugin_lifecycle.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -14,8 +15,13 @@ #include #include #include "plugin_test_utils.hpp" +#include "test_utils.hpp" #include +#include +#include +#include +#include #include #include @@ -1068,3 +1074,43 @@ TEST_CASE("Unloading a plugin that is not loaded is a no-op", "[PluginLifecycle] CHECK(manager.unload_plugin("No_Such_Plugin")); CHECK_FALSE(manager.is_plugin_loaded("No_Such_Plugin")); } + +namespace { + +// CA certificates in the default client context, as a plugin's urllib or http.client request would +// verify against. +int default_context_ca_count() +{ + pybind11::gil_scoped_acquire gil; + const pybind11::dict stats = pybind11::module_::import("ssl").attr("create_default_context")().attr("cert_store_stats")(); + return stats["x509_ca"].cast(); +} + +} // namespace + +TEST_CASE("The bundled Python trusts the system CA certificates", "[PluginLifecycle][Python]") +{ + // The app runs this before plugins load; on Linux it is what points OpenSSL at the distro bundle. + Http::tls_global_init(); + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + CHECK(default_context_ca_count() > 0); +} + +#ifndef _WIN32 // Windows adds its system store whatever SSL_CERT_FILE names. +TEST_CASE("SSL_CERT_FILE still overrides the bundled Python's default CA certificates", "[PluginLifecycle][Python]") +{ + // Existing but empty: Linux's CurlGlobalInit replaces an SSL_CERT_FILE that names a missing file. + ScopedTemporaryFile empty_bundle(".pem"); + std::ofstream(empty_bundle.string()).close(); + ScopedEnvVar ssl_cert_file{"SSL_CERT_FILE", empty_bundle.string()}; + + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + CHECK(default_context_ca_count() == 0); +} +#endif diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index d0817ce128..cffa05cb15 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -10,10 +10,13 @@ #include #include +#include #include #include +#include #include +#include #if defined(WIN32) || defined(_WIN32) #define PATH_SEPARATOR R"(\)" @@ -197,4 +200,27 @@ private: boost::filesystem::path m_previous; }; +// Sets an environment variable for the lifetime of the guard and restores the previous value. +class ScopedEnvVar +{ +public: + ScopedEnvVar(std::string name, const std::string& value) : m_name(std::move(name)) + { + if (const char* old = boost::nowide::getenv(m_name.c_str())) + m_previous = old; + boost::nowide::setenv(m_name.c_str(), value.c_str(), 1); + } + ~ScopedEnvVar() + { + if (m_previous) + boost::nowide::setenv(m_name.c_str(), m_previous->c_str(), 1); + else + boost::nowide::unsetenv(m_name.c_str()); + } + +private: + std::string m_name; + std::optional m_previous; +}; + #endif // SLIC3R_TEST_UTILS