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
This commit is contained in:
SoftFever authored and GitHub committed 2026-10-19 20:06:14 +08:00
1 parent 3b9b871ba6
commit e5b6cbf091
5 files changed
+94 -36

No files matched your search

+17 -7
View File
@@ -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
# <prefix>/lib64 while every other dep uses <prefix>/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
+5 -4
View File
@@ -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.
@@ -12,7 +12,6 @@
#include <boost/asio/write.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/nowide/cstdlib.hpp>
#include <boost/system/error_code.hpp>
#include <atomic>
@@ -23,7 +22,6 @@
#include <istream>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
@@ -196,29 +194,6 @@ private:
std::vector<std::shared_ptr<Connection>> 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<std::string> 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
@@ -5,6 +5,7 @@
#include <ios>
#include <libslic3r/LifecycleEvents.hpp>
#include <memory>
#include <slic3r/Utils/Http.hpp>
#include <slic3r/Utils/NetworkAgentFactory.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginManager.hpp>
@@ -14,8 +15,13 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_message.hpp>
#include "plugin_test_utils.hpp"
#include "test_utils.hpp"
#include <boost/filesystem.hpp>
#include <pybind11/cast.h>
#include <pybind11/gil.h>
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <algorithm>
#include <chrono>
@@ -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<int>();
}
} // 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
+26
View File
@@ -10,10 +10,13 @@
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/cstdlib.hpp>
#include <cstdio>
#include <fstream>
#include <optional>
#include <string>
#include <utility>
#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<std::string> m_previous;
};
#endif // SLIC3R_TEST_UTILS