//! Who is calling? Every command resolves the calling webview to a role before it does //! anything. Module windows are registered by the module runtime when it creates them; //! an unknown or closed window is rejected like `UNTRUSTED_PLUGIN_SENDER` in 0.1.0. use crate::core::error::{CoreError, CoreResult}; use parking_lot::RwLock; use serde_json::Value; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; pub const MAIN_LABEL: &str = "main"; pub const LICENSE_LABEL: &str = "license"; pub const MODULE_LABEL_PREFIX: &str = "module-"; #[derive(Clone, Debug)] pub struct ModuleContext { pub id: String, pub label: String, pub manifest: Value, pub permissions: Vec, pub root: PathBuf, } impl ModuleContext { pub fn require(&self, permission: &str) -> CoreResult<()> { if self.permissions.iter().any(|p| p == permission) { Ok(()) } else { Err(CoreError::new("PLUGIN_PERMISSION_DENIED", format!("Plugin permission denied: {permission}"))) } } } #[derive(Default)] pub struct WindowRegistry { modules: RwLock>>, } impl WindowRegistry { pub fn register_module(&self, context: ModuleContext) -> Arc { let context = Arc::new(context); self.modules.write().insert(context.label.clone(), context.clone()); context } pub fn unregister_module(&self, label: &str) -> Option> { self.modules.write().remove(label) } pub fn module_by_label(&self, label: &str) -> Option> { self.modules.read().get(label).cloned() } pub fn module_by_id(&self, id: &str) -> Option> { self.modules.read().values().find(|c| c.id == id).cloned() } pub fn modules(&self) -> Vec> { self.modules.read().values().cloned().collect() } } /// Origin checks: the main and license windows may only run the bundled app pages. pub fn is_app_url(url: &url::Url) -> bool { match url.scheme() { "tauri" => url.host_str() == Some("localhost"), "http" | "https" => url.host_str() == Some("tauri.localhost"), _ => false, } } pub fn require_main(label: &str, url: Option) -> CoreResult<()> { if label == MAIN_LABEL && url.as_ref().map(is_app_url).unwrap_or(false) { Ok(()) } else { Err(CoreError::new("UNTRUSTED_IPC_SENDER", "Untrusted IPC sender.")) } } pub fn require_license(label: &str, url: Option) -> CoreResult<()> { if label == LICENSE_LABEL && url.as_ref().map(is_app_url).unwrap_or(false) { Ok(()) } else { Err(CoreError::plain("Untrusted license IPC sender.")) } } /// The page of a module window must still be the module's own entry origin. pub fn module_url_matches(url: &url::Url, module_id: &str) -> bool { match url.scheme() { "chj-module" => url.host_str() == Some(module_id), "http" | "https" => { url.host_str() == Some("chj-module.localhost") && url.path_segments().and_then(|mut s| s.next()) == Some(module_id) } _ => false, } } pub fn require_module(registry: &WindowRegistry, label: &str, url: Option) -> CoreResult> { let untrusted = || CoreError::new("UNTRUSTED_PLUGIN_SENDER", "Untrusted plugin IPC sender."); if !label.starts_with(MODULE_LABEL_PREFIX) { return Err(untrusted()); } let context = registry.module_by_label(label).ok_or_else(untrusted)?; match url { Some(url) if module_url_matches(&url, &context.id) => Ok(context), _ => Err(untrusted()), } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn module_identity_and_permissions() { let registry = WindowRegistry::default(); registry.register_module(ModuleContext { id: "chj.users".into(), label: "module-1".into(), manifest: json!({}), permissions: vec!["users.read".into()], root: PathBuf::new(), }); let ok = require_module(®istry, "module-1", url::Url::parse("chj-module://chj.users/ui/index.html").ok()).unwrap(); assert!(ok.require("users.read").is_ok()); assert_eq!(ok.require("users.manage").unwrap_err().code, "PLUGIN_PERMISSION_DENIED"); assert!(require_module(®istry, "module-1", url::Url::parse("chj-module://chj.disks/ui/index.html").ok()).is_err()); assert!(require_module(®istry, "module-2", url::Url::parse("chj-module://chj.users/").ok()).is_err()); assert!(require_module(®istry, "module-1", url::Url::parse("http://chj-module.localhost/chj.users/ui/index.html").ok()).is_ok()); assert!(require_main("main", url::Url::parse("tauri://localhost/index.html").ok()).is_ok()); assert!(require_main("main", url::Url::parse("https://evil.example/").ok()).is_err()); assert!(require_main("module-1", url::Url::parse("tauri://localhost/").ok()).is_err()); } }