diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index ac0a1bd96b..b6cb12d21d 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -22,6 +22,7 @@ jobs: date: ver: ver_pure: + ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }} steps: - name: Checkout @@ -317,7 +318,7 @@ jobs: - name: Install dependencies from build_linux.sh if: inputs.os == 'ubuntu-20.04' || inputs.os == 'ubuntu-24.04' shell: bash - run: sudo ./build_linux.sh -ur + run: sudo env "ORCA_UPDATER_SIG_KEY=$ORCA_UPDATER_SIG_KEY" ./build_linux.sh -ur - name: Fix permissions if: inputs.os == 'ubuntu-20.04' || inputs.os == 'ubuntu-24.04' diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..5446ab270e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +# Repository Guidelines + +## Project Structure & Module Organization +OrcaSlicer’s C++17 sources live in `src/`, split by feature modules and platform adapters. User assets, icons, and printer presets are in `resources/`; translations stay in `localization/`. Tests sit in `tests/`, grouped by domain (`libslic3r/`, `sla_print/`, etc.) with fixtures under `tests/data/`. CMake helpers reside in `cmake/`, and longer references in `doc/` and `SoftFever_doc/`. Automation scripts belong in `scripts/` and `tools/`. Treat everything in `deps/` and `deps_src/` as vendored snapshots—do not modify without mirroring upstream tags. + +## Build, Test, and Development Commands +Use out-of-source builds: +- `cmake -S . -B build -DCMAKE_BUILD_TYPE=Release` configures dependencies and generates build files. +- `cmake --build build --target OrcaSlicer --config Release` compiles the app; add `--parallel` to speed up. +- `cmake --build build --target tests` then `ctest --test-dir build --output-on-failure` runs automated suites. +Platform helpers such as `build_linux.sh`, `build_release_macos.sh`, and `build_release_vs2022.bat` wrap the same flow with toolchain flags. Use `build_release_macos.sh -sx` when reproducing macOS build issues, and `scripts/DockerBuild.sh` for reproducible container builds. + +## Coding Style & Naming Conventions +`.clang-format` enforces 4-space indents, a 140-column limit, aligned initializers, and brace wrapping for classes and functions. Run `clang-format -i ` before committing; the CMake `clang-format` target is available when LLVM tools are on your PATH. Prefer `CamelCase` for classes, `snake_case` for functions and locals, and `SCREAMING_CASE` for constants, matching conventions in `src/`. Keep headers self-contained and align include order with the IWYU pragmas. + +## Testing Guidelines +Unit tests rely on Catch2 (`tests/catch2/`). Name specs after the component under test—for example `tests/libslic3r/TestPlanarHole.cpp`—and tag long-running cases so `ctest -L fast` remains useful. Cover new algorithms with deterministic fixtures or sample G-code stored in `tests/data/`. Document manual printer validation or regression slicer checks in your PR when automated coverage is insufficient. + +## Commit & Pull Request Guidelines +The history favors concise, sentence-style subject lines with optional issue references, e.g., `Fix grid lines origin for multiple plates (#10724)`. Squash fixups locally before opening a PR. Complete `.github/pull_request_template.md`, include reproduction steps or screenshots for UI changes, and mention impacted presets or translations. Link issues via `Closes #NNNN` when applicable, and call out dependency bumps or profile migrations for maintainer review. + +## Security & Configuration Tips +Follow `SECURITY.md` for vulnerability reporting. Keep API tokens and printer credentials out of tracked configs; use `sandboxes/` for experimental settings. When touching third-party code in `deps_src/`, record the upstream commit or release in your PR description and run the relevant platform build script to confirm integration. diff --git a/build_linux.sh b/build_linux.sh index 5be72253fb..a696177938 100755 --- a/build_linux.sh +++ b/build_linux.sh @@ -225,6 +225,9 @@ if [[ -n "${BUILD_ORCA}" ]] ; then if [[ -n "${BUILD_TESTS}" ]] ; then BUILD_ARGS+=(-DBUILD_TESTS=ON) fi + if [[ -n "${ORCA_UPDATER_SIG_KEY}" ]] ; then + BUILD_ARGS+=(-DORCA_UPDATER_SIG_KEY="${ORCA_UPDATER_SIG_KEY}") + fi echo "Configuring OrcaSlicer..." set -x diff --git a/build_release.bat b/build_release.bat index 05d38a52bc..6317e277c9 100644 --- a/build_release.bat +++ b/build_release.bat @@ -24,6 +24,8 @@ cd deps mkdir %build_dir% cd %build_dir% set DEPS=%CD%/OrcaSlicer_dep +set "SIG_FLAG=" +if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%" if "%1"=="slicer" ( GOTO :slicer ) @@ -42,7 +44,7 @@ mkdir %build_dir% cd %build_dir% echo cmake .. -G "Visual Studio 16 2019" -A x64 -DBBL_RELEASE_TO_PUBLIC=1 -DCMAKE_PREFIX_PATH="%DEPS%/usr/local" -DCMAKE_INSTALL_PREFIX="./OrcaSlicer" -DCMAKE_BUILD_TYPE=%build_type% -cmake .. -G "Visual Studio 16 2019" -A x64 -DBBL_RELEASE_TO_PUBLIC=1 -DCMAKE_PREFIX_PATH="%DEPS%/usr/local" -DCMAKE_INSTALL_PREFIX="./OrcaSlicer" -DCMAKE_BUILD_TYPE=%build_type% -DWIN10SDK_PATH="C:/Program Files (x86)/Windows Kits/10/Include/10.0.19041.0" +cmake .. -G "Visual Studio 16 2019" -A x64 -DBBL_RELEASE_TO_PUBLIC=1 %SIG_FLAG% -DCMAKE_PREFIX_PATH="%DEPS%/usr/local" -DCMAKE_INSTALL_PREFIX="./OrcaSlicer" -DCMAKE_BUILD_TYPE=%build_type% -DWIN10SDK_PATH="C:/Program Files (x86)/Windows Kits/10/Include/10.0.19041.0" cmake --build . --config %build_type% --target ALL_BUILD -- -m cd .. call scripts/run_gettext.bat diff --git a/build_release_macos.sh b/build_release_macos.sh index 4ef49bbc6e..e336da09c7 100755 --- a/build_release_macos.sh +++ b/build_release_macos.sh @@ -174,6 +174,7 @@ function build_slicer() { -G "${SLICER_CMAKE_GENERATOR}" \ -DBBL_RELEASE_TO_PUBLIC=1 \ -DORCA_TOOLS=ON \ + ${ORCA_UPDATER_SIG_KEY:+-DORCA_UPDATER_SIG_KEY="$ORCA_UPDATER_SIG_KEY"} \ -DCMAKE_PREFIX_PATH="$DEPS/usr/local" \ -DCMAKE_INSTALL_PREFIX="$PWD/OrcaSlicer" \ -DCMAKE_BUILD_TYPE="$BUILD_CONFIG" \ diff --git a/build_release_vs2022.bat b/build_release_vs2022.bat index c7a225a684..b9c377488f 100644 --- a/build_release_vs2022.bat +++ b/build_release_vs2022.bat @@ -38,6 +38,8 @@ cd deps mkdir %build_dir% cd %build_dir% set DEPS=%CD%/OrcaSlicer_dep +set "SIG_FLAG=" +if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%" if "%1"=="slicer" ( GOTO :slicer @@ -58,7 +60,7 @@ mkdir %build_dir% cd %build_dir% echo on -cmake .. -G "Visual Studio 17 2022" -A x64 -DBBL_RELEASE_TO_PUBLIC=1 -DORCA_TOOLS=ON -DCMAKE_PREFIX_PATH="%DEPS%/usr/local" -DCMAKE_INSTALL_PREFIX="./OrcaSlicer" -DCMAKE_BUILD_TYPE=%build_type% -DWIN10SDK_PATH="%WindowsSdkDir%Include\%WindowsSDKVersion%\" +cmake .. -G "Visual Studio 17 2022" -A x64 -DBBL_RELEASE_TO_PUBLIC=1 -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_PREFIX_PATH="%DEPS%/usr/local" -DCMAKE_INSTALL_PREFIX="./OrcaSlicer" -DCMAKE_BUILD_TYPE=%build_type% -DWIN10SDK_PATH="%WindowsSdkDir%Include\%WindowsSDKVersion%\" cmake --build . --config %build_type% --target ALL_BUILD -- -m @echo off cd .. diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 38cd03a8ac..68eed40e0f 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -37,8 +37,7 @@ using namespace nlohmann; namespace Slic3r { -static const std::string VERSION_CHECK_URL_STABLE = "https://api.github.com/repos/softfever/OrcaSlicer/releases/latest"; -static const std::string VERSION_CHECK_URL = "https://api.github.com/repos/softfever/OrcaSlicer/releases"; +static const std::string VERSION_CHECK_URL = "https://check-version.orcaslicer.com/latest"; static const std::string PROFILE_UPDATE_URL = "https://api.github.com/repos/OrcaSlicer/orcaslicer-profiles/releases/tags"; static const std::string MODELS_STR = "models"; @@ -1383,10 +1382,10 @@ std::string AppConfig::config_path() return path; } -std::string AppConfig::version_check_url(bool stable_only/* = false*/) const +std::string AppConfig::version_check_url() const { auto from_settings = get("version_check_url"); - return from_settings.empty() ? stable_only ? VERSION_CHECK_URL_STABLE : VERSION_CHECK_URL : from_settings; + return from_settings.empty() ? VERSION_CHECK_URL : from_settings; } std::string AppConfig::profile_update_url() const diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 96fca84e23..aef70c06d6 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -294,7 +294,7 @@ public: // Get the Slic3r version check url. // This returns a hardcoded string unless it is overriden by "version_check_url" in the ini file. - std::string version_check_url(bool stable_only = false) const; + std::string version_check_url() const; // Get the Orca profile update url. std::string profile_update_url() const; diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 8af6530b8a..7f0e1e2b17 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -537,6 +537,8 @@ set(SLIC3R_GUI_SOURCES Utils/HexFile.hpp Utils/Http.cpp Utils/Http.hpp + Utils/InstanceID.cpp + Utils/InstanceID.hpp Utils/json_diff.cpp Utils/json_diff.hpp Utils/minilzo_extension.cpp @@ -615,11 +617,28 @@ if (UNIX AND NOT APPLE) ) endif () +set(ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY}") +string(STRIP "${ORCA_UPDATER_SIG_KEY_B64}" ORCA_UPDATER_SIG_KEY_B64) +string(REPLACE "\n" "" ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY_B64}") +string(REPLACE "\r" "" ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY_B64}") +string(REPLACE "\t" "" ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY_B64}") +string(REPLACE " " "" ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY_B64}") + +set(ORCA_UPDATER_SIG_KEY_AVAILABLE 0) +if(ORCA_UPDATER_SIG_KEY_B64) + set(ORCA_UPDATER_SIG_KEY_AVAILABLE 1) +endif() + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/GeneratedConfig.hpp.in + ${CMAKE_CURRENT_BINARY_DIR}/GeneratedConfig.hpp + @ONLY) + add_library(libslic3r_gui STATIC ${SLIC3R_GUI_SOURCES}) -target_include_directories(libslic3r_gui PRIVATE Utils) +target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DIR}) if (WIN32) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include) + target_link_libraries(libslic3r_gui Advapi32) endif() source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES}) @@ -645,7 +664,7 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") ${CURL_LIBRARIES} ) elseif (APPLE) - target_link_libraries(libslic3r_gui ${DISKARBITRATION_LIBRARY}) + target_link_libraries(libslic3r_gui ${DISKARBITRATION_LIBRARY} "-framework Security") endif() if (SLIC3R_STATIC) @@ -670,7 +689,9 @@ endif () # layer and sub-libraries. This forces us to use the include locations and # link these libraries. if (UNIX AND NOT APPLE) + find_package(PkgConfig REQUIRED) find_package(GTK${SLIC3R_GTK} REQUIRED) + pkg_check_modules(LIBSECRET REQUIRED libsecret-1) if (FLATPAK) # I don't know why this is needed, but for whatever reason slic3r isn't # linking to X11 and webkit2gtk. force it. @@ -679,8 +700,8 @@ if (UNIX AND NOT APPLE) pkg_check_modules(webkit2gtk REQUIRED webkit2gtk-4.1) target_link_libraries (libslic3r_gui ${X11_LIBRARIES} ${webkit2gtk_LIBRARIES}) endif() - target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS}) - target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig) + target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS} ${LIBSECRET_INCLUDE_DIRS}) + target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig ${LIBSECRET_LIBRARIES}) # We add GStreamer for bambu:/// support. diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index f27d9dbd39..96199a0cb1 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -54,6 +55,9 @@ #include #include #include +#include +#include +#include #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" @@ -70,12 +74,14 @@ #include "MainFrame.hpp" #include "Plater.hpp" #include "GLCanvas3D.hpp" +#include "GeneratedConfig.hpp" #include "../Utils/PresetUpdater.hpp" #include "../Utils/PrintHost.hpp" #include "../Utils/Process.hpp" #include "../Utils/MacDarkMode.hpp" #include "../Utils/Http.hpp" +#include "../Utils/InstanceID.hpp" #include "../Utils/UndoRedo.hpp" #include "slic3r/Config/Snapshot.hpp" #include "Preferences.hpp" @@ -4381,108 +4387,414 @@ Semver get_version(const std::string& str, const std::regex& regexp) { return Semver::invalid(); } +namespace +{ + +struct UpdaterQuery +{ + std::string iid; + std::string version; + std::string os; + std::string arch; + std::string os_info; +}; + +std::string detect_updater_os() +{ +#if defined(_WIN32) + return "win"; +#elif defined(__APPLE__) + return "macos"; +#elif defined(__linux__) || defined(__LINUX__) + return "linux"; +#else + return "unknown"; +#endif +} + +std::string detect_updater_arch() +{ +#if defined(__aarch64__) || defined(_M_ARM64) + return "arm64"; +#elif defined(__x86_64__) || defined(_M_X64) + return "x86_64"; +#elif defined(__i386__) || defined(_M_IX86) + return "i386"; +#else + std::string arch = wxPlatformInfo::Get().GetArchName().ToStdString(); + boost::algorithm::to_lower(arch); + if (arch.find("aarch64") != std::string::npos || arch.find("arm64") != std::string::npos) + return "arm64"; + if (arch.find("x86_64") != std::string::npos || arch.find("amd64") != std::string::npos) + return "x86_64"; + if (arch.find("i686") != std::string::npos || arch.find("i386") != std::string::npos || arch.find("x86") != std::string::npos) + return "i386"; + return "unknown"; +#endif +} + +std::string detect_updater_os_info() +{ + wxString description = wxPlatformInfo::Get().GetOperatingSystemDescription(); +#if defined(__LINUX__) || defined(__linux__) + wxLinuxDistributionInfo distro = wxGetLinuxDistributionInfo(); + if (!distro.Id.empty()) { + wxString normalized = distro.Id; + if (!distro.Release.empty()) + normalized << " " << distro.Release; + normalized.Trim(true); + normalized.Trim(false); + if (!normalized.empty()) + description = normalized; + } +#endif + if (description.empty()) + description = wxGetOsDescription(); + + //Orca: workaround: wxGetOsVersion can't recognize Windows 11 + // For Windows, use actual version numbers to properly detect Windows 11 + // Windows 11 starts at build 22000 +#if defined(_WIN32) + int major = 0, minor = 0, micro = 0; + wxGetOsVersion(&major, &minor, µ); + if (micro >= 22000) { + // replace Windows 10 with Windows 11 + description.Replace("Windows 10", "Windows 11"); + } +#endif + std::string os_info = description.ToStdString(); + boost::replace_all(os_info, "\r", " "); + boost::replace_all(os_info, "\n", " "); + boost::algorithm::trim(os_info); + if (os_info.size() > 120) + os_info.resize(120); + boost::algorithm::to_lower(os_info); + return os_info; +} + +std::string detect_updater_version() +{ + return SoftFever_VERSION; +} + +std::string detect_updater_iid(AppConfig* config) +{ + if (config == nullptr) + return {}; + return instance_id::ensure(*config); +} + +std::string encode_uri_component(const std::string& value) +{ + static constexpr const char* hex = "0123456789ABCDEF"; + std::string out; + out.reserve(value.size()); + for (unsigned char ch : value) { + if ((ch >= 'A' && ch <= 'Z') || + (ch >= 'a' && ch <= 'z') || + (ch >= '0' && ch <= '9') || + ch == '-' || ch == '_' || ch == '.' || ch == '~' || + ch == '!' || ch == '*' || ch == '(' || ch == ')' || ch == '\'') { + out.push_back(static_cast(ch)); + } else { + out.push_back('%'); + out.push_back(hex[(ch >> 4) & 0xF]); + out.push_back(hex[ch & 0xF]); + } + } + return out; +} + +std::string build_updater_query(const UpdaterQuery& query) +{ + std::vector> params; + + auto add_param = [¶ms](const char* key, const std::string& value) { + if (!value.empty()) + params.emplace_back(key, encode_uri_component(value)); + }; + + add_param("iid", query.iid); + add_param("v", query.version); + add_param("os", query.os); + add_param("arch", query.arch); + add_param("os_info", query.os_info); + + std::sort(params.begin(), params.end(), [](const auto& lhs, const auto& rhs) { + return lhs.first < rhs.first; + }); + + if (params.empty()) + return {}; + + std::string encoded; + for (size_t idx = 0; idx < params.size(); ++idx) { + if (idx > 0) + encoded.push_back('&'); + encoded += params[idx].first; + encoded.push_back('='); + encoded += params[idx].second; + } + return encoded; +} + +std::string base64url_encode(const unsigned char* data, std::size_t length) +{ + std::string encoded; + encoded.resize(boost::beast::detail::base64::encoded_size(length)); + encoded.resize(boost::beast::detail::base64::encode(encoded.data(), data, length)); + std::replace(encoded.begin(), encoded.end(), '+', '-'); + std::replace(encoded.begin(), encoded.end(), '/', '_'); + while (!encoded.empty() && encoded.back() == '=') + encoded.pop_back(); + return encoded; +} + +std::optional> load_signature_key() +{ +#if ORCA_UPDATER_SIG_KEY_AVAILABLE + std::string key = ORCA_UPDATER_SIG_KEY_B64; + boost::algorithm::trim(key); + if (key.empty()) + return std::nullopt; + + key.erase(std::remove_if(key.begin(), key.end(), [](unsigned char ch) { return std::isspace(ch); }), key.end()); + std::replace(key.begin(), key.end(), '-', '+'); + std::replace(key.begin(), key.end(), '_', '/'); + while (key.size() % 4 != 0) + key.push_back('='); + + std::string decoded; + decoded.resize(boost::beast::detail::base64::decoded_size(key.size())); + auto decode_result = boost::beast::detail::base64::decode(decoded.data(), key.data(), key.size()); + if (!decode_result.second) + return std::nullopt; + decoded.resize(decode_result.first); + + return std::vector(decoded.begin(), decoded.end()); +#else + return std::nullopt; +#endif +} + +const std::optional>& get_signature_key() +{ + static std::optional> cached; + static bool loaded = false; + if (!loaded) { + cached = load_signature_key(); + loaded = true; + } + return cached; +} + +std::string extract_path_from_url(const std::string& url) +{ + if (url.empty()) + return "/latest"; + + std::string path; + const auto scheme_pos = url.find("://"); + if (scheme_pos != std::string::npos) { + const auto path_pos = url.find('/', scheme_pos + 3); + if (path_pos != std::string::npos) + path = url.substr(path_pos); + else + path = "/"; + } else { + path = url; + } + + const auto fragment_pos = path.find('#'); + if (fragment_pos != std::string::npos) + path = path.substr(0, fragment_pos); + + const auto query_pos = path.find('?'); + if (query_pos != std::string::npos) + path = path.substr(0, query_pos); + + if (path.empty()) + path = "/"; + return path; +} + +void maybe_attach_updater_signature(Http& http, const std::string& canonical_query, const std::string& request_url) +{ + if (canonical_query.empty()) + return; + + const auto& key = get_signature_key(); + if (!key || key->empty()) + return; + + const auto now = std::chrono::time_point_cast(std::chrono::system_clock::now()); + const std::string timestamp = std::to_string(now.time_since_epoch().count()); + const std::string path = extract_path_from_url(request_url); + + std::string string_to_sign = "GET\n"; + string_to_sign += path; + string_to_sign += "\n"; + string_to_sign += canonical_query; + string_to_sign += "\n"; + string_to_sign += timestamp; + + unsigned int digest_length = 0; + unsigned char digest[EVP_MAX_MD_SIZE] = {}; + if (HMAC(EVP_sha256(), key->data(), static_cast(key->size()), + reinterpret_cast(string_to_sign.data()), + string_to_sign.size(), digest, &digest_length) == nullptr || digest_length == 0) + return; + + const std::string signature = base64url_encode(digest, digest_length); + http.header("X-Orca-Ts", timestamp); + http.header("X-Orca-Sig", "v1:" + signature); +} + +} // namespace + void GUI_App::check_new_version_sf(bool show_tips, int by_user) { AppConfig* app_config = wxGetApp().app_config; bool check_stable_only = app_config->get_bool("check_stable_update_only"); - auto version_check_url = app_config->version_check_url(check_stable_only); - Http::get(version_check_url) + auto version_check_url = app_config->version_check_url(); + + UpdaterQuery query{ + detect_updater_iid(app_config), + detect_updater_version(), + detect_updater_os(), + detect_updater_arch(), + detect_updater_os_info() + }; + + const std::string query_string = build_updater_query(query); + if (!query_string.empty()) { + const bool has_query = version_check_url.find('?') != std::string::npos; + if (!has_query) + version_check_url.push_back('?'); + else if (!version_check_url.empty() && version_check_url.back() != '&' && version_check_url.back() != '?') + version_check_url.push_back('&'); + version_check_url += query_string; + } + + auto http = Http::get(version_check_url); + maybe_attach_updater_signature(http, query_string, version_check_url); + + http.header("accept", "application/vnd.github.v3+json") + .timeout_connect(5) + .timeout_max(10) .on_error([&](std::string body, std::string error, unsigned http_status) { (void)body; BOOST_LOG_TRIVIAL(error) << format("Error getting: `%1%`: HTTP %2%, %3%", "check_new_version_sf", http_status, error); }) - .timeout_connect(1) - .on_complete([this,by_user, check_stable_only](std::string body, unsigned http_status) { - // Http response OK + .on_complete([this, by_user, check_stable_only](std::string body, unsigned http_status) { if (http_status != 200) return; try { boost::trim(body); - // Orca: parse github release, inspired by SS - boost::property_tree::ptree root; - std::stringstream json_stream(body); - boost::property_tree::read_json(json_stream, root); - - // at least two number, use '.' as separator. can be followed by -Az23 for prereleased and +Az42 for - // metadata - std::regex matcher("[0-9]+\\.[0-9]+(\\.[0-9]+)*(-[A-Za-z0-9]+)?(\\+[A-Za-z0-9]+)?"); - - Semver current_version = get_version(SoftFever_VERSION, matcher); - Semver best_pre(1, 0, 0); - Semver best_release(1, 0, 0); - std::string best_pre_url; - std::string best_release_url; - std::string best_release_content; - std::string best_pre_content; - const std::regex reg_num("([0-9]+)"); - if (check_stable_only) { - std::string tag = root.get("tag_name"); - if (tag[0] == 'v') - tag.erase(0, 1); - for (std::regex_iterator it = std::sregex_iterator(tag.begin(), tag.end(), reg_num); it != std::sregex_iterator(); ++it) {} - Semver tag_version = get_version(tag, matcher); - if (root.get("prerelease")) { - if (best_pre < tag_version) { - best_pre = tag_version; - best_pre_url = root.get("html_url"); - best_pre_content = root.get("body"); - } - } else { - if (best_release < tag_version) { - best_release = tag_version; - best_release_url = root.get("html_url"); - best_release_content = root.get("body"); - } - } - } else { - for (auto json_version : root) { - std::string tag = json_version.second.get("tag_name"); - if (tag[0] == 'v') - tag.erase(0, 1); - for (std::regex_iterator it = std::sregex_iterator(tag.begin(), tag.end(), reg_num); it != std::sregex_iterator(); - ++it) {} - Semver tag_version = get_version(tag, matcher); - if (json_version.second.get("prerelease")) { - if (best_pre < tag_version) { - best_pre = tag_version; - best_pre_url = json_version.second.get("html_url"); - best_pre_content = json_version.second.get("body"); - } - } else { - if (best_release < tag_version) { - best_release = tag_version; - best_release_url = json_version.second.get("html_url"); - best_release_content = json_version.second.get("body"); - } - } - } - } - - // if release is more recent than beta, use release anyway - if (best_pre < best_release) { - best_pre = best_release; - best_pre_url = best_release_url; - best_pre_content = best_release_content; - } - // if we're the most recent, don't do anything - if ((check_stable_only ? best_release : best_pre) <= current_version) { + if (body.empty()) { if (by_user != 0) this->no_new_version(); return; } - version_info.url = check_stable_only ? best_release_url : best_pre_url; - version_info.version_str = check_stable_only ? best_release.to_string_sf() : best_pre.to_string(); - version_info.description = check_stable_only ? best_release_content : best_pre_content; + boost::property_tree::ptree root; + std::stringstream json_stream(body); + boost::property_tree::read_json(json_stream, root); + + std::regex matcher("[0-9]+\\.[0-9]+(\\.[0-9]+)*(-[A-Za-z0-9]+)?(\\+[A-Za-z0-9]+)?"); + Semver current_version = get_version(SoftFever_VERSION, matcher); + Semver best_pre(0, 0, 0); + Semver best_release(0, 0, 0); + bool best_pre_valid = false; + bool best_release_valid = false; + std::string best_pre_url; + std::string best_release_url; + std::string best_release_content; + std::string best_pre_content; + + auto consider_release = [&](const boost::property_tree::ptree& node) { + auto tag_opt = node.get_optional("tag_name"); + if (!tag_opt) + return; + + std::string tag = *tag_opt; + if (!tag.empty() && tag.front() == 'v') + tag.erase(0, 1); + + Semver tag_version = get_version(tag, matcher); + if (!tag_version.valid()) + return; + + const bool is_prerelease = node.get_optional("prerelease").get_value_or(false); + const std::string html_url = node.get_optional("html_url").get_value_or(std::string()); + const std::string body_copy = node.get_optional("body").get_value_or(std::string()); + + if (is_prerelease) { + if (!best_pre_valid || best_pre < tag_version) { + best_pre = tag_version; + best_pre_url = html_url; + best_pre_content = body_copy; + best_pre_valid = true; + } + } else { + if (!best_release_valid || best_release < tag_version) { + best_release = tag_version; + best_release_url = html_url; + best_release_content = body_copy; + best_release_valid = true; + } + } + }; + + if (root.get_optional("tag_name")) { + consider_release(root); + } else { + for (const auto& child : root) + consider_release(child.second); + } + + if (!best_release_valid && !best_pre_valid) { + if (by_user != 0) + this->no_new_version(); + return; + } + + if (best_pre_valid && best_release_valid && best_pre < best_release) { + best_pre = best_release; + best_pre_url = best_release_url; + best_pre_content = best_release_content; + best_pre_valid = true; + } + + const bool prefer_release = check_stable_only || !best_pre_valid; + const Semver& chosen_version = prefer_release ? best_release : best_pre; + const bool chosen_valid = prefer_release ? best_release_valid : best_pre_valid; + + if (!chosen_valid) { + if (by_user != 0) + this->no_new_version(); + return; + } + + if (current_version.valid() && chosen_version <= current_version) { + if (by_user != 0) + this->no_new_version(); + return; + } + + version_info.url = prefer_release ? best_release_url : best_pre_url; + version_info.version_str = prefer_release ? best_release.to_string_sf() : best_pre.to_string(); + version_info.description = prefer_release ? best_release_content : best_pre_content; version_info.force_upgrade = false; wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); - evt->SetString((check_stable_only ? best_release : best_pre).to_string()); + evt->SetString((prefer_release ? best_release : best_pre).to_string()); GUI::wxGetApp().QueueEvent(evt); } catch (...) {} - }) - .perform(); + }); + + http.perform(); } void GUI_App::process_network_msg(std::string dev_id, std::string msg) diff --git a/src/slic3r/GeneratedConfig.hpp.in b/src/slic3r/GeneratedConfig.hpp.in new file mode 100644 index 0000000000..130f113034 --- /dev/null +++ b/src/slic3r/GeneratedConfig.hpp.in @@ -0,0 +1,5 @@ +#pragma once + +#define ORCA_UPDATER_SIG_KEY_B64 "@ORCA_UPDATER_SIG_KEY_B64@" +#define ORCA_UPDATER_SIG_KEY_AVAILABLE @ORCA_UPDATER_SIG_KEY_AVAILABLE@ + diff --git a/src/slic3r/Utils/InstanceID.cpp b/src/slic3r/Utils/InstanceID.cpp new file mode 100644 index 0000000000..d4a903e276 --- /dev/null +++ b/src/slic3r/Utils/InstanceID.cpp @@ -0,0 +1,209 @@ +#include "InstanceID.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "libslic3r/AppConfig.hpp" +#include "libslic3r/Utils.hpp" + +namespace Slic3r { +namespace instance_id { +namespace { + +constexpr const char* CONFIG_KEY = "updater_iid"; +constexpr const char* LEGACY_KEY = "iid"; + +std::mutex& cache_mutex() +{ + static std::mutex mtx; + return mtx; +} + +std::string& cached_iid() +{ + static std::string value; + return value; +} + +bool& cache_ready() +{ + static bool ready = false; + return ready; +} + +std::optional normalize_uuid(std::string value) +{ + boost::algorithm::trim(value); + boost::algorithm::to_lower(value); + if (value.size() != 36) + return std::nullopt; + try { + const boost::uuids::uuid parsed = boost::uuids::string_generator()(value); + if (parsed.version() != boost::uuids::uuid::version_random_number_based) + return std::nullopt; + return value; + } catch (...) { + return std::nullopt; + } +} + +std::optional read_config_value(AppConfig& config) +{ + const auto read_key = [&](const char* key) -> std::optional { + const std::string raw = config.get(key); + if (raw.empty()) + return std::nullopt; + if (auto normalized = normalize_uuid(raw)) + return normalized; + return std::nullopt; + }; + + if (auto value = read_key(CONFIG_KEY)) + return value; + return read_key(LEGACY_KEY); +} + +void write_config_value(AppConfig& config, const std::string& value) +{ + config.set(CONFIG_KEY, value); + if (config.get(LEGACY_KEY) != value) + config.set(LEGACY_KEY, value); +} + +void prune_config_value(AppConfig& config) +{ + if (config.has(CONFIG_KEY)) + config.erase("app", CONFIG_KEY); + if (config.has(LEGACY_KEY)) + config.erase("app", LEGACY_KEY); +} + +boost::filesystem::path storage_path() +{ + const std::string& base_dir = Slic3r::data_dir(); + if (base_dir.empty()) + return {}; + return boost::filesystem::path(base_dir) / ".orcaslicer_machine_id"; +} + +std::optional read_storage_file() +{ + const auto path = storage_path(); + if (path.empty() || !boost::filesystem::exists(path)) + return std::nullopt; + + boost::nowide::ifstream file(path.string()); + if (!file) + return std::nullopt; + + std::string value; + std::getline(file, value); + file.close(); + + return normalize_uuid(value); +} + +bool write_storage_file(const std::string& value) +{ + if (value.empty()) + return false; + + const auto path = storage_path(); + if (path.empty()) + return false; + + const auto parent = path.parent_path(); + boost::system::error_code ec; + if (!parent.empty() && !boost::filesystem::exists(parent)) + boost::filesystem::create_directories(parent, ec); + + if (ec) + return false; + + boost::nowide::ofstream file(path.string(), std::ios::trunc); + if (!file) + return false; + + file << value; + file.close(); + + return file.good(); +} + +std::optional read_secure() +{ + return read_storage_file(); +} + +bool write_secure(const std::string& value) +{ + return write_storage_file(value); +} + +std::string generate_uuid() +{ + const auto uuid = boost::uuids::random_generator()(); + return boost::uuids::to_string(uuid); +} + +} // namespace + +std::string ensure(AppConfig& config) +{ + std::lock_guard lock(cache_mutex()); + if (cache_ready()) + return cached_iid(); + + if (auto secure = read_secure()) { + cached_iid() = *secure; + cache_ready() = true; + prune_config_value(config); + return cached_iid(); + } + + if (auto from_config = read_config_value(config)) { + cached_iid() = *from_config; + cache_ready() = true; + if (!write_secure(cached_iid())) + write_config_value(config, cached_iid()); + else + prune_config_value(config); + return cached_iid(); + } + + cached_iid() = generate_uuid(); + cache_ready() = true; + + if (!write_secure(cached_iid())) + write_config_value(config, cached_iid()); + else + prune_config_value(config); + + return cached_iid(); +} + +void reset_cache_for_tests() +{ + std::lock_guard lock(cache_mutex()); + cached_iid().clear(); + cache_ready() = false; +} + +} // namespace instance_id +} // namespace Slic3r diff --git a/src/slic3r/Utils/InstanceID.hpp b/src/slic3r/Utils/InstanceID.hpp new file mode 100644 index 0000000000..112e4af2b4 --- /dev/null +++ b/src/slic3r/Utils/InstanceID.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace Slic3r { + +class AppConfig; + +namespace instance_id { + +// Returns the canonical IID, generating and storing one when missing. +std::string ensure(AppConfig& config); + +// Clears the cached IID (primarily for tests). +void reset_cache_for_tests(); + +} // namespace instance_id + +} // namespace Slic3r + diff --git a/version.inc b/version.inc index fa0de080fe..4af1e2cccb 100644 --- a/version.inc +++ b/version.inc @@ -18,3 +18,7 @@ set(ORCA_VERSION_MINOR ${CMAKE_MATCH_2}) set(ORCA_VERSION_PATCH ${CMAKE_MATCH_3}) set(SLIC3R_VERSION "01.10.01.50") + +if (NOT DEFINED ORCA_UPDATER_SIG_KEY) + set(ORCA_UPDATER_SIG_KEY "" CACHE STRING "Base64url encoded updater signature key") +endif()