From a717572d653818bbf90eed23dff8a430eb9fa68e Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 29 Jul 2026 15:00:07 +0300 Subject: [PATCH] fix(plugins): trust platform certificates Use the operating system trust store and Windows proxy settings while keeping TLS verification enabled. Add retryable registry errors with copyable diagnostics.\n\nRefs #563 --- Cargo.lock | 216 +++++++++++++++++++++++++++++--- Cargo.toml | 2 +- src/app/mod.rs | 17 +++ src/app/update/mod.rs | 42 ++++++- src/app/view/modal.rs | 3 + src/discussions.rs | 5 +- src/io/update_check.rs | 5 +- src/lib.rs | 2 + src/main.rs | 2 + src/network.rs | 35 ++++++ src/patreon.rs | 5 +- src/plugin/marketplace.rs | 7 +- src/scene/model/image_model.rs | 5 +- src/ui/window/plugin_manager.rs | 127 ++++++++++++++++++- src/videos.rs | 5 +- 15 files changed, 434 insertions(+), 44 deletions(-) create mode 100644 src/network.rs diff --git a/Cargo.lock b/Cargo.lock index 1c9d9466..d7a56a63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,7 +233,7 @@ dependencies = [ "android-properties", "bitflags 2.13.1", "cc", - "jni", + "jni 0.22.4", "libc", "log", "ndk", @@ -919,6 +919,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -2595,6 +2601,22 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -3085,7 +3107,7 @@ dependencies = [ "dispatch", "futures-channel", "futures-lite", - "jni", + "jni 0.22.4", "ndk-context", "objc2 0.6.4", "objc2-app-kit 0.3.2", @@ -3765,6 +3787,12 @@ dependencies = [ "libc", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "orbclient" version = "0.3.55" @@ -4701,6 +4729,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.15.0" @@ -4710,6 +4750,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.21.1", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" @@ -4799,6 +4866,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -4830,6 +4906,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.3.0" @@ -5835,9 +5934,11 @@ dependencies = [ "percent-encoding", "rustls", "rustls-pki-types", + "rustls-platform-verifier", "ureq-proto", "utf8-zero", "webpki-roots", + "winreg", ] [[package]] @@ -6187,6 +6288,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.8" @@ -6409,7 +6519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ "windows-core 0.58.0", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -6443,7 +6553,7 @@ dependencies = [ "windows-interface 0.58.0", "windows-result 0.2.0", "windows-strings 0.1.0", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -6536,7 +6646,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -6555,7 +6665,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ "windows-result 0.2.0", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -6567,13 +6677,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -6582,7 +6701,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -6594,20 +6713,35 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -6619,18 +6753,36 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6643,24 +6795,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6737,6 +6913,16 @@ dependencies = [ "memchr 2.8.3", ] +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "winresource" version = "0.1.31" diff --git a/Cargo.toml b/Cargo.toml index dcaad6fa..86f4d77a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,7 +108,7 @@ open = "5" # PDF export / printing. Native-only: printpdf pulls a wasm-incompatible # `memchr` (1.0.2) via lopdf → nom_locate, and the web build has no filesystem. printpdf = { version = "0.9.1", default-features = false } -ureq = { version = "3", default-features = false, features = ["rustls"] } +ureq = { version = "3", default-features = false, features = ["rustls", "platform-verifier", "win-system-proxy"] } [target.'cfg(target_arch = "wasm32")'.dependencies] # Web gets the dependency-free manifest/ribbon contract only (no plugin host). diff --git a/src/app/mod.rs b/src/app/mod.rs index 5d1aa6b0..ae20c4c8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -524,6 +524,14 @@ pub(super) struct OpenCADStudio { loaded_plugin_ids: rustc_hash::FxHashSet, /// Curated plugin registry fetched from the OpenCADStudio repo. plugin_registry: Vec, + /// True while the curated registry request is in flight. + plugin_registry_loading: bool, + /// Last curated-registry request error. Kept separate from general + /// marketplace status so the UI can show a friendly retry card and retain + /// copyable technical details. + plugin_registry_error: Option, + /// Whether the registry error card exposes its raw diagnostic text. + plugin_registry_error_details_open: bool, /// User-linked plugin source repos (`owner/repo`) beyond the curated list. plugin_repos: Vec, /// Add-repository text field in the Plugin Manager. @@ -2076,6 +2084,12 @@ pub enum Message { PluginRepoRemove(String), /// The curated registry was fetched. PluginRegistryFetched(Result, String>), + /// Retry the curated registry request after a connection failure. + PluginRegistryRetry, + /// Expand or collapse raw registry error details. + PluginRegistryErrorDetailsToggle, + /// Copy registry URL, platform, version, and raw error details. + PluginRegistryCopyDiagnostics, /// Patreon supporters fetched at boot for the Start page (name, pledge cents). PatronsFetched(Result, String>), /// Tutorial-playlist videos fetched at boot for the Start page. @@ -2609,6 +2623,9 @@ impl OpenCADStudio { external_plugins: Vec::new(), loaded_plugin_ids: rustc_hash::FxHashSet::default(), plugin_registry: Vec::new(), + plugin_registry_loading: false, + plugin_registry_error: None, + plugin_registry_error_details_open: false, plugin_repos: Vec::new(), plugin_repo_input: String::new(), plugin_search_input: String::new(), diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index eeecf083..b38fb60c 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -3956,6 +3956,9 @@ impl OpenCADStudio { // Fetch the curated registry and release lists for linked repos. #[cfg(not(target_arch = "wasm32"))] { + self.plugin_registry_loading = true; + self.plugin_registry_error = None; + self.plugin_registry_error_details_open = false; let mut tasks = vec![self.fetch_registry_task()]; let release_repos: rustc_hash::FxHashSet = self .plugin_repos @@ -4074,6 +4077,9 @@ impl OpenCADStudio { Task::none() } Message::PluginRegistryFetched(Ok(entries)) => { + self.plugin_registry_loading = false; + self.plugin_registry_error = None; + self.plugin_registry_error_details_open = false; // Fetch releases for every curated repo so the dropdowns fill in. #[cfg(not(target_arch = "wasm32"))] { @@ -4112,7 +4118,41 @@ impl OpenCADStudio { } } Message::PluginRegistryFetched(Err(e)) => { - self.marketplace_status = format!("Registry: {e}"); + self.plugin_registry_loading = false; + self.plugin_registry_error = Some(e); + self.plugin_registry_error_details_open = false; + Task::none() + } + Message::PluginRegistryRetry => { + #[cfg(not(target_arch = "wasm32"))] + { + self.plugin_registry_loading = true; + self.plugin_registry_error = None; + self.plugin_registry_error_details_open = false; + return self.fetch_registry_task(); + } + #[cfg(target_arch = "wasm32")] + Task::none() + } + Message::PluginRegistryErrorDetailsToggle => { + if self.plugin_registry_error.is_some() { + self.plugin_registry_error_details_open = + !self.plugin_registry_error_details_open; + } + Task::none() + } + Message::PluginRegistryCopyDiagnostics => { + #[cfg(not(target_arch = "wasm32"))] + if let Some(error) = &self.plugin_registry_error { + return iced::clipboard::write(format!( + "Open CAD Studio v{}\nOS: {}\nArchitecture: {}\nRegistry: {}\nError: {}", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + std::env::consts::ARCH, + crate::plugin::marketplace::REGISTRY_URL, + error, + )); + } Task::none() } Message::PatronsFetched(Ok(names)) => { diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 60ab761b..2388eb72 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -78,6 +78,9 @@ impl OpenCADStudio { &self.loaded_plugin_ids, crate::ui::window::plugin_manager::MarketView { registry: &self.plugin_registry, + registry_loading: self.plugin_registry_loading, + registry_error: self.plugin_registry_error.as_deref(), + registry_error_details_open: self.plugin_registry_error_details_open, input: &self.plugin_repo_input, search: &self.plugin_search_input, repos: &self.plugin_repos, diff --git a/src/discussions.rs b/src/discussions.rs index 43b73312..23dc730e 100644 --- a/src/discussions.rs +++ b/src/discussions.rs @@ -61,10 +61,7 @@ pub fn load_cached() -> Vec { #[cfg(not(target_arch = "wasm32"))] pub fn fetch_discussions() -> Result, String> { - let agent: ureq::Agent = ureq::Agent::config_builder() - .timeout_global(Some(std::time::Duration::from_secs(15))) - .build() - .into(); + let agent = crate::network::agent(std::time::Duration::from_secs(15)); let feed = get_text(&agent, FEED_URL)?; let page = get_text(&agent, DISCUSSIONS_URL)?; let pinned = pinned_numbers(&page); diff --git a/src/io/update_check.rs b/src/io/update_check.rs index 8d4b613d..81d317d1 100644 --- a/src/io/update_check.rs +++ b/src/io/update_check.rs @@ -48,10 +48,7 @@ pub async fn check_for_update() -> Option { #[cfg(not(target_arch = "wasm32"))] fn fetch_latest_if_outdated() -> Option { - let agent: ureq::Agent = ureq::Agent::config_builder() - .timeout_global(Some(std::time::Duration::from_secs(5))) - .build() - .into(); + let agent = crate::network::agent(std::time::Duration::from_secs(5)); let body = agent .get(RELEASES_API) .header("User-Agent", concat!("OpenCADStudio/", env!("CARGO_PKG_VERSION"))) diff --git a/src/lib.rs b/src/lib.rs index ea1711c0..eb9e2d5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,8 @@ pub mod cli; pub mod command; pub mod entities; pub mod io; +#[cfg(not(target_arch = "wasm32"))] +pub(crate) mod network; pub mod modules; pub mod patreon; pub mod discussions; diff --git a/src/main.rs b/src/main.rs index c92a56c8..158e28e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ mod cli; mod command; mod entities; mod io; +#[cfg(not(target_arch = "wasm32"))] +mod network; mod modules; mod patreon; mod discussions; diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 00000000..6f45a641 --- /dev/null +++ b/src/network.rs @@ -0,0 +1,35 @@ +//! Shared native HTTP client configuration. +//! +//! Desktop requests use the operating system's certificate verifier so roots +//! installed by administrators, corporate proxies, and security software are +//! honoured without weakening TLS verification. + +#![cfg(not(target_arch = "wasm32"))] + +use std::time::Duration; +use ureq::tls::{RootCerts, TlsConfig}; + +pub(crate) fn agent(timeout: Duration) -> ureq::Agent { + let tls = TlsConfig::builder() + .root_certs(RootCerts::PlatformVerifier) + .build(); + ureq::Agent::config_builder() + .timeout_global(Some(timeout)) + .tls_config(tls) + .build() + .into() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn agent_uses_platform_verifier_without_disabling_tls() { + let agent = agent(Duration::from_secs(1)); + let tls = agent.config().tls_config(); + + assert!(matches!(tls.root_certs(), &RootCerts::PlatformVerifier)); + assert!(!tls.disable_verification()); + } +} diff --git a/src/patreon.rs b/src/patreon.rs index 01eba0a6..0c605760 100644 --- a/src/patreon.rs +++ b/src/patreon.rs @@ -47,10 +47,7 @@ pub fn fetch_patrons() -> Result, String> { .filter(|t| !t.is_empty()) .ok_or("no Patreon token configured")?; - let agent: ureq::Agent = ureq::Agent::config_builder() - .timeout_global(Some(std::time::Duration::from_secs(15))) - .build() - .into(); + let agent = crate::network::agent(std::time::Duration::from_secs(15)); // The token is creator-scoped, so its first campaign is the one to list. let campaigns = get_json(&agent, token, "https://www.patreon.com/api/oauth2/v2/campaigns")?; diff --git a/src/plugin/marketplace.rs b/src/plugin/marketplace.rs index 86c882d7..6009df43 100644 --- a/src/plugin/marketplace.rs +++ b/src/plugin/marketplace.rs @@ -16,7 +16,7 @@ use super::external; use super::external::RegistryEntry; /// The curated registry, read from the OpenCADStudio repo's `main` branch. -const REGISTRY_URL: &str = +pub(crate) const REGISTRY_URL: &str = "https://raw.githubusercontent.com/HakanSeven12/OpenCADStudio/main/plugins/registry.json"; /// Fetch the curated plugin registry (`plugins/registry.json`). @@ -104,10 +104,7 @@ fn external_lib_ext() -> &'static str { } fn agent() -> ureq::Agent { - ureq::Agent::config_builder() - .timeout_global(Some(std::time::Duration::from_secs(15))) - .build() - .into() + crate::network::agent(std::time::Duration::from_secs(15)) } const UA: &str = concat!("OpenCADStudio/", env!("CARGO_PKG_VERSION")); diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index 4958daec..2c2c29f4 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -434,10 +434,7 @@ fn decode_reference(path: &str) -> Option { /// synchronous fetch and would hit CORS on a cross-origin image anyway. #[cfg(not(target_arch = "wasm32"))] fn fetch_remote(url: &str) -> Option> { - let agent: ureq::Agent = ureq::Agent::config_builder() - .timeout_global(Some(std::time::Duration::from_secs(8))) - .build() - .into(); + let agent = crate::network::agent(std::time::Duration::from_secs(8)); let mut resp = agent .get(url) .header( diff --git a/src/ui/window/plugin_manager.rs b/src/ui/window/plugin_manager.rs index efc4921b..cc3d617f 100644 --- a/src/ui/window/plugin_manager.rs +++ b/src/ui/window/plugin_manager.rs @@ -19,6 +19,9 @@ const SCROLLBAR_GUTTER: f32 = 16.0; /// Marketplace state passed to the Plugin Manager view. pub struct MarketView<'a> { pub registry: &'a [RegistryEntry], + pub registry_loading: bool, + pub registry_error: Option<&'a str>, + pub registry_error_details_open: bool, pub input: &'a str, pub search: &'a str, pub repos: &'a [String], @@ -407,12 +410,119 @@ fn add_repository_card<'a>(m: &MarketView) -> Element<'a, Message> { ) } +fn registry_error_message(error: &str) -> (&'static str, &'static str) { + let error = error.to_ascii_lowercase(); + if error.contains("certificate") + || error.contains("unknownissuer") + || error.contains("unknown issuer") + { + ( + "Unable to verify the server certificate", + "Open CAD Studio could not trust the certificate presented for the plugin registry. \ + Check your system certificate and proxy settings, then retry.", + ) + } else if error.contains("timed out") || error.contains("timeout") { + ( + "Plugin registry request timed out", + "Check your internet or proxy connection, then retry. Manually added repositories \ + remain available.", + ) + } else { + ( + "Unable to load the plugin registry", + "Check your internet or proxy connection, then retry. Manually added repositories \ + remain available.", + ) + } +} + +fn registry_notice<'a>(m: &MarketView) -> Option> { + if let Some(error) = m.registry_error { + let (title, message) = registry_error_message(error); + let actions = row![ + pill_button( + "Retry", + Message::PluginRegistryRetry, + button::primary, + ), + Space::new().width(6), + pill_button( + if m.registry_error_details_open { + "Hide details" + } else { + "Show details" + }, + Message::PluginRegistryErrorDetailsToggle, + button::secondary, + ), + Space::new().width(6), + pill_button( + "Copy details", + Message::PluginRegistryCopyDiagnostics, + button::secondary, + ), + ] + .align_y(iced::Center); + let mut body = column![ + text(title).size(13), + text(message).size(11).style(muted_style), + Space::new().height(3), + actions, + ] + .spacing(5); + if m.registry_error_details_open { + body = body.push( + container(text(error.to_string()).size(10).width(Fill)) + .padding(8) + .width(Fill) + .style(container::bordered_box), + ); + } + return Some( + container(body.padding([10, 12])) + .width(Fill) + .style(|theme: &Theme| { + let pair = theme.extended_palette().warning.weak; + container::Style { + background: Some(Background::Color(pair.color.scale_alpha(0.16))), + border: Border { + color: pair.color, + width: 1.0, + radius: 6.0.into(), + }, + ..Default::default() + } + }) + .into(), + ); + } + + (m.registry_loading && m.registry.is_empty()).then(|| { + container( + column![ + text("Loading plugin catalog…").size(13), + text("Connecting securely using your system certificate settings.") + .size(11) + .style(muted_style), + ] + .spacing(5) + .padding([10, 12]), + ) + .width(Fill) + .style(container::bordered_box) + .into() + }) +} + fn marketplace_section<'a>( m: &MarketView, externals: &[ExternalPlugin], ) -> Element<'a, Message> { let mut col = column![text("Available plugins").size(13).style(primary_style)].spacing(6); let mut visible = 0usize; + if let Some(notice) = registry_notice(m) { + col = col.push(notice); + } // Curated registry entries (from the OpenCADStudio repo). for e in m.registry { @@ -482,7 +592,10 @@ fn marketplace_section<'a>( )); } - if visible == 0 { + if visible == 0 + && m.registry_error.is_none() + && !(m.registry_loading && m.registry.is_empty()) + { let message = if m.search.trim().is_empty() { "No additional plugins are available." } else { @@ -731,7 +844,7 @@ pub fn view_window<'a>( #[cfg(test)] mod tests { - use super::{newest_update, repository_display_name}; + use super::{newest_update, registry_error_message, repository_display_name}; #[test] fn newest_update_uses_semver_not_release_order() { @@ -751,4 +864,14 @@ mod tests { "OpenCAD Storm Sewer Plugin", ); } + + #[test] + fn certificate_errors_get_user_friendly_copy() { + let (title, message) = + registry_error_message("io: invalid peer certificate: UnknownIssuer"); + + assert_eq!(title, "Unable to verify the server certificate"); + assert!(message.contains("system certificate")); + assert!(!message.contains("UnknownIssuer")); + } } diff --git a/src/videos.rs b/src/videos.rs index fef6defa..50f1d2f2 100644 --- a/src/videos.rs +++ b/src/videos.rs @@ -65,10 +65,7 @@ pub fn load_cached() -> Vec { /// keeps whatever [`load_cached`] provided. #[cfg(not(target_arch = "wasm32"))] pub fn fetch_playlist() -> Result, String> { - let agent: ureq::Agent = ureq::Agent::config_builder() - .timeout_global(Some(std::time::Duration::from_secs(15))) - .build() - .into(); + let agent = crate::network::agent(std::time::Duration::from_secs(15)); let page = agent .get(PLAYLIST_URL) .header("User-Agent", "Mozilla/5.0")