Compare commits

...

2 Commits

Author SHA1 Message Date
Gregory Schier
67cbb06bb9 Use native TLS when certificate validation is disabled for legacy server compatibility
When "Validate TLS certificates" is disabled, use the OS native TLS stack
(Secure Transport/SChannel/OpenSSL) instead of rustls. This adds support for
TLS 1.0+ connections to legacy servers like IBM WebSphere, which rustls cannot
handle since it only implements TLS 1.2+.

Ref: https://yaak.app/feedback/posts/tls-handshake-eof-when-connecting-to-private-ibm-websphere-endpoint-works-when-s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 21:33:47 -07:00
Gregory Schier
c8ba35e268 Gracefully handle plugin init failures (#424)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:55:46 -07:00
9 changed files with 129 additions and 16 deletions

1
Cargo.lock generated
View File

@@ -10447,6 +10447,7 @@ dependencies = [
"hyper-util",
"log 0.4.29",
"mime_guess",
"native-tls",
"regex 1.11.1",
"reqwest",
"serde",

View File

@@ -1383,13 +1383,12 @@ async fn cmd_reload_plugins<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> {
) -> YaakResult<Vec<(String, String)>> {
let plugins = app_handle.db().list_plugins()?;
let plugin_context =
PluginContext::new(Some(window.label().to_string()), window.workspace_id());
let _errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await;
// Note: errors are returned but we don't show toasts here since this is a manual reload
Ok(())
let errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await;
Ok(errors)
}
#[tauri::command]
@@ -1731,6 +1730,7 @@ pub fn run() {
git_ext::cmd_git_rm_remote,
//
// Plugin commands
plugins_ext::cmd_plugin_init_errors,
plugins_ext::cmd_plugins_install_from_directory,
plugins_ext::cmd_plugins_search,
plugins_ext::cmd_plugins_install,

View File

@@ -198,6 +198,13 @@ pub async fn cmd_plugins_uninstall<R: Runtime>(
Ok(delete_and_uninstall(plugin_manager, &query_manager, &plugin_context, plugin_id).await?)
}
#[command]
pub async fn cmd_plugin_init_errors(
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<(String, String)>> {
Ok(plugin_manager.take_init_errors().await)
}
#[command]
pub async fn cmd_plugins_updates<R: Runtime>(
app_handle: AppHandle<R>,
@@ -306,7 +313,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
dev_mode,
)
.await
.expect("Failed to initialize plugins");
.expect("Failed to start plugin runtime");
app_handle_clone.manage(manager);
});

View File

@@ -18,8 +18,9 @@ zstd = "0.13"
hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] }
log = { workspace = true }
mime_guess = "2.0.5"
native-tls = "0.2"
regex = "1.11.1"
reqwest = { workspace = true, features = ["rustls-tls-manual-roots-no-provider", "socks", "http2", "stream"] }
reqwest = { workspace = true, features = ["rustls-tls-manual-roots-no-provider", "native-tls", "socks", "http2", "stream"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }

View File

@@ -6,6 +6,56 @@ use std::sync::Arc;
use yaak_models::models::DnsOverride;
use yaak_tls::{ClientCertificateConfig, get_tls_config};
/// Build a native-tls connector for maximum compatibility when certificate
/// validation is disabled. Unlike rustls, native-tls uses the OS TLS stack
/// (Secure Transport on macOS, SChannel on Windows, OpenSSL on Linux) which
/// supports TLS 1.0+ for legacy servers.
fn build_native_tls_connector(
client_cert: Option<ClientCertificateConfig>,
) -> Result<native_tls::TlsConnector> {
let mut builder = native_tls::TlsConnector::builder();
builder.danger_accept_invalid_certs(true);
builder.danger_accept_invalid_hostnames(true);
builder.min_protocol_version(Some(native_tls::Protocol::Tlsv10));
if let Some(identity) = build_native_tls_identity(client_cert)? {
builder.identity(identity);
}
Ok(builder.build()?)
}
fn build_native_tls_identity(
client_cert: Option<ClientCertificateConfig>,
) -> Result<Option<native_tls::Identity>> {
let config = match client_cert {
None => return Ok(None),
Some(c) => c,
};
// Try PFX/PKCS12 first
if let Some(pfx_path) = &config.pfx_file {
if !pfx_path.is_empty() {
let pfx_data = std::fs::read(pfx_path)?;
let password = config.passphrase.as_deref().unwrap_or("");
let identity = native_tls::Identity::from_pkcs12(&pfx_data, password)?;
return Ok(Some(identity));
}
}
// Try CRT + KEY files
if let (Some(crt_path), Some(key_path)) = (&config.crt_file, &config.key_file) {
if !crt_path.is_empty() && !key_path.is_empty() {
let crt_data = std::fs::read(crt_path)?;
let key_data = std::fs::read(key_path)?;
let identity = native_tls::Identity::from_pkcs8(&crt_data, &key_data)?;
return Ok(Some(identity));
}
}
Ok(None)
}
#[derive(Clone)]
pub struct HttpConnectionProxySettingAuth {
pub user: String,
@@ -51,10 +101,17 @@ impl HttpConnectionOptions {
// This is needed so we can emit DNS timing events for each request
.pool_max_idle_per_host(0);
// Configure TLS with optional client certificate
let config =
get_tls_config(self.validate_certificates, true, self.client_certificate.clone())?;
client = client.use_preconfigured_tls(config);
// Configure TLS
if self.validate_certificates {
// Use rustls with platform certificate verification (TLS 1.2+ only)
let config = get_tls_config(true, true, self.client_certificate.clone())?;
client = client.use_preconfigured_tls(config);
} else {
// Use native TLS for maximum compatibility (supports TLS 1.0+)
let connector =
build_native_tls_connector(self.client_certificate.clone())?;
client = client.use_preconfigured_tls(connector);
}
// Configure DNS resolver - keep a reference to configure per-request
let resolver = LocalhostResolver::new(self.dns_overrides.clone());

View File

@@ -9,6 +9,12 @@ pub enum Error {
#[error(transparent)]
TlsError(#[from] yaak_tls::error::Error),
#[error("Native TLS error: {0}")]
NativeTlsError(#[from] native_tls::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Request failed with {0:?}")]
RequestError(String),

View File

@@ -50,6 +50,8 @@ pub struct PluginManager {
vendored_plugin_dir: PathBuf,
pub(crate) installed_plugin_dir: PathBuf,
dev_mode: bool,
/// Errors from plugin initialization, retrievable once via `take_init_errors`.
init_errors: Arc<Mutex<Vec<(String, String)>>>,
}
/// Callback for plugin initialization events (e.g., toast notifications)
@@ -93,6 +95,7 @@ impl PluginManager {
vendored_plugin_dir,
installed_plugin_dir,
dev_mode,
init_errors: Default::default(),
};
// Forward events to subscribers
@@ -183,17 +186,21 @@ impl PluginManager {
let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await;
if !init_errors.is_empty() {
let joined = init_errors
.into_iter()
.map(|(dir, err)| format!("{dir}: {err}"))
.collect::<Vec<_>>()
.join("; ");
return Err(PluginErr(format!("Failed to initialize plugin(s): {joined}")));
for (dir, err) in &init_errors {
warn!("Plugin failed to initialize: {dir}: {err}");
}
*plugin_manager.init_errors.lock().await = init_errors;
}
Ok(plugin_manager)
}
/// Take any initialization errors, clearing them from the manager.
/// Returns a list of `(plugin_directory, error_message)` pairs.
pub async fn take_init_errors(&self) -> Vec<(String, String)> {
std::mem::take(&mut *self.init_errors.lock().await)
}
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
pub fn get_plugins_dir(&self) -> PathBuf {
if self.dev_mode {

View File

@@ -123,6 +123,39 @@ export function initGlobalListeners() {
console.log('Got plugin updates event', payload);
showPluginUpdatesToast(payload);
});
// Check for plugin initialization errors
invokeCmd<[string, string][]>('cmd_plugin_init_errors').then((errors) => {
for (const [dir, message] of errors) {
const dirBasename = dir.split('/').pop() ?? dir;
showToast({
id: `plugin-init-error-${dirBasename}`,
color: 'warning',
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">Plugin failed to load</h2>
<p className="text-text-subtle text-sm">
{dirBasename}: {message}
</p>
</VStack>
),
action: ({ hide }) => (
<Button
size="xs"
color="warning"
variant="border"
onClick={() => {
hide();
openSettings.mutate('plugins:installed');
}}
>
View Plugins
</Button>
),
});
}
});
}
function showUpdateInstalledToast(version: string) {

View File

@@ -42,6 +42,7 @@ type TauriCmd =
| 'cmd_new_child_window'
| 'cmd_new_main_window'
| 'cmd_plugin_info'
| 'cmd_plugin_init_errors'
| 'cmd_reload_plugins'
| 'cmd_render_template'
| 'cmd_save_response'