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
This commit is contained in:
Hakan Seven committed 2026-07-29 15:00:07 +03:00
1 parent 4e25e88c31
commit a717572d65
15 files changed
+434 -44

No files matched your search

+17
View File
@@ -524,6 +524,14 @@ pub(super) struct OpenCADStudio {
loaded_plugin_ids: rustc_hash::FxHashSet<String>,
/// Curated plugin registry fetched from the OpenCADStudio repo.
plugin_registry: Vec<crate::plugin::external::RegistryEntry>,
/// 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<String>,
/// 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<String>,
/// Add-repository text field in the Plugin Manager.
@@ -2076,6 +2084,12 @@ pub enum Message {
PluginRepoRemove(String),
/// The curated registry was fetched.
PluginRegistryFetched(Result<Vec<crate::plugin::external::RegistryEntry>, 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<Vec<(String, i64)>, 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(),
+41 -1
View File
@@ -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<String> = 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)) => {
+3
View File
@@ -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,
+1 -4
View File
@@ -61,10 +61,7 @@ pub fn load_cached() -> Vec<DiscussionEntry> {
#[cfg(not(target_arch = "wasm32"))]
pub fn fetch_discussions() -> Result<Vec<DiscussionEntry>, 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);
+1 -4
View File
@@ -48,10 +48,7 @@ pub async fn check_for_update() -> Option<UpdateInfo> {
#[cfg(not(target_arch = "wasm32"))]
fn fetch_latest_if_outdated() -> Option<UpdateInfo> {
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")))
+2
View File
@@ -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;
+2
View File
@@ -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;
+35
View File
@@ -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());
}
}
+1 -4
View File
@@ -47,10 +47,7 @@ pub fn fetch_patrons() -> Result<Vec<(String, i64)>, 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")?;
+2 -5
View File
@@ -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"));
+1 -4
View File
@@ -434,10 +434,7 @@ fn decode_reference(path: &str) -> Option<DecodedImage> {
/// synchronous fetch and would hit CORS on a cross-origin image anyway.
#[cfg(not(target_arch = "wasm32"))]
fn fetch_remote(url: &str) -> Option<Vec<u8>> {
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(
+125 -2
View File
@@ -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<Element<'a, Message>> {
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"));
}
}
+1 -4
View File
@@ -65,10 +65,7 @@ pub fn load_cached() -> Vec<VideoEntry> {
/// keeps whatever [`load_cached`] provided.
#[cfg(not(target_arch = "wasm32"))]
pub fn fetch_playlist() -> Result<Vec<VideoEntry>, 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")