From ed467c275b56317e4cbd9cb6a27cfecb999aef46 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sun, 24 Apr 2022 15:18:22 -0700 Subject: [PATCH] perf: completely remove endpoints if none of its APIs is allowlisted (#3958) --- core/tauri-runtime-wry/src/lib.rs | 6 +- core/tauri/Cargo.toml | 3 +- core/tauri/build.rs | 248 +++++++++++++++++------------ core/tauri/src/api/file.rs | 2 + core/tauri/src/endpoints.rs | 51 +++++- core/tauri/src/endpoints/window.rs | 123 +++++++------- core/tauri/src/lib.rs | 91 ++++++++++- core/tauri/src/window.rs | 8 +- 8 files changed, 364 insertions(+), 168 deletions(-) diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index b230a5b84..12a65d62f 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -141,8 +141,10 @@ impl WebviewIdStore { #[macro_export] macro_rules! getter { ($self: ident, $rx: expr, $message: expr) => {{ - crate::send_user_message(&$self.context, $message)?; - $rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage) + $crate::send_user_message(&$self.context, $message)?; + $rx + .recv() + .map_err(|_| $crate::Error::FailedToReceiveMessage) }}; } diff --git a/core/tauri/Cargo.toml b/core/tauri/Cargo.toml index 7bac7e299..bccef72d8 100644 --- a/core/tauri/Cargo.toml +++ b/core/tauri/Cargo.toml @@ -104,7 +104,8 @@ version = "0.30.0" features = [ "Win32_Foundation" ] [build-dependencies] -cfg_aliases = "0.1.1" +heck = "0.4" +once_cell = "1.10" [dev-dependencies] mockito = "0.31" diff --git a/core/tauri/build.rs b/core/tauri/build.rs index e0a798284..0288c5626 100644 --- a/core/tauri/build.rs +++ b/core/tauri/build.rs @@ -2,108 +2,160 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use cfg_aliases::cfg_aliases; +use heck::ToSnakeCase; +use once_cell::sync::OnceCell; -fn main() { - cfg_aliases! { - custom_protocol: { feature = "custom-protocol" }, - dev: { not(feature = "custom-protocol") }, - updater: { any(feature = "updater", feature = "__updater-docs") }, +use std::{path::Path, sync::Mutex}; - api_all: { feature = "api-all" }, +static CHECKED_FEATURES: OnceCell>> = OnceCell::new(); - // fs - fs_all: { any(api_all, feature = "fs-all") }, - fs_read_file: { any(fs_all, feature = "fs-read-file") }, - fs_write_file: { any(fs_all, feature = "fs-write-file") }, - fs_write_binary_file: { any(fs_all, feature = "fs-write-binary-file") }, - fs_read_dir: { any(fs_all, feature = "fs-read-dir") }, - fs_copy_file: { any(fs_all, feature = "fs-copy-file") }, - fs_create_dir: { any(fs_all, feature = "fs-create_dir") }, - fs_remove_dir: { any(fs_all, feature = "fs-remove-dir") }, - fs_remove_file: { any(fs_all, feature = "fs-remove-file") }, - fs_rename_file: { any(fs_all, feature = "fs-rename-file") }, +// checks if the given Cargo feature is enabled. +fn has_feature(feature: &str) -> bool { + CHECKED_FEATURES + .get_or_init(Default::default) + .lock() + .unwrap() + .push(feature.to_string()); - // window - window_all: { any(api_all, feature = "window-all") }, - window_create: { any(window_all, feature = "window-create") }, - window_center: { any(window_all, feature = "window-center") }, - window_request_user_attention: { any(window_all, feature = "window-request-user-attention") }, - window_set_resizable: { any(window_all, feature = "window-set-resizable") }, - window_set_title: { any(window_all, feature = "window-set-title") }, - window_maximize: { any(window_all, feature = "window-maximize") }, - window_unmaximize: { any(window_all, feature = "window-unmaximize") }, - window_minimize: { any(window_all, feature = "window-minimize") }, - window_unminimize: { any(window_all, feature = "window-unminimize") }, - window_show: { any(window_all, feature = "window-show") }, - window_hide: { any(window_all, feature = "window-hide") }, - window_close: { any(window_all, feature = "window-close") }, - window_set_decorations: { any(window_all, feature = "window-set-decorations") }, - window_set_always_on_top: { any(window_all, feature = "window-set-always-on-top") }, - window_set_size: { any(window_all, feature = "window-set-size") }, - window_set_min_size: { any(window_all, feature = "window-set-min-size") }, - window_set_max_size: { any(window_all, feature = "window-set-max-size") }, - window_set_position: { any(window_all, feature = "window-set-position") }, - window_set_fullscreen: { any(window_all, feature = "window-set-fullscreen") }, - window_set_focus: { any(window_all, feature = "window-set-focus") }, - window_set_icon: { any(window_all, feature = "window-set-icon") }, - window_set_skip_taskbar: { any(window_all, feature = "window-set-skip-taskbar") }, - window_set_cursor_grab: { any(window_all, feature = "window-set-cursor-grab") }, - window_set_cursor_visible: { any(window_all, feature = "window-set-cursor-visible") }, - window_set_cursor_icon: { any(window_all, feature = "window-set-cursor-icon") }, - window_set_cursor_position: { any(window_all, feature = "window-set-cursor-position") }, - window_start_dragging: { any(window_all, feature = "window-start-dragging") }, - window_print: { any(window_all, feature = "window-print") }, + // when a feature is enabled, Cargo sets the `CARGO_FEATURE_-` +// and aliased as `_`. +// +// The `-all` feature is also aliased to `_all`. +// +// If any of the features is enabled, the `_any` alias is created. +// +// Note that both `module` and `apis` strings must be written in kebab case. +fn alias_module(module: &str, apis: &[&str], api_all: bool) { + let all_feature_name = format!("{}-all", module); + let all = api_all || has_feature(&all_feature_name); + alias(&all_feature_name.to_snake_case(), all); + + let mut any = all; + + for api in apis { + let has = all || has_feature(&format!("{}-{}", module, api)); + alias( + &format!("{}_{}", module.to_snake_case(), api.to_snake_case()), + has, + ); + any = any || has; + } + + alias(&format!("{}_any", module.to_snake_case()), any); +} diff --git a/core/tauri/src/api/file.rs b/core/tauri/src/api/file.rs index 67c4dbc7e..4de9f8b0e 100644 --- a/core/tauri/src/api/file.rs +++ b/core/tauri/src/api/file.rs @@ -34,10 +34,12 @@ impl SafePathBuf { } } + #[allow(dead_code)] pub unsafe fn new_unchecked(path: std::path::PathBuf) -> Self { Self(path) } + #[allow(dead_code)] pub fn display(&self) -> Display<'_> { self.0.display() } diff --git a/core/tauri/src/endpoints.rs b/core/tauri/src/endpoints.rs index af79c43cc..2c3444715 100644 --- a/core/tauri/src/endpoints.rs +++ b/core/tauri/src/endpoints.rs @@ -13,17 +13,27 @@ use serde_json::Value as JsonValue; use std::sync::Arc; mod app; +#[cfg(cli)] mod cli; +#[cfg(clipboard_any)] mod clipboard; +#[cfg(dialog_any)] mod dialog; mod event; +#[cfg(fs_any)] mod file_system; +#[cfg(global_shortcut_any)] mod global_shortcut; +#[cfg(http_any)] mod http; mod notification; +#[cfg(os_any)] mod operating_system; +#[cfg(path_any)] mod path; +#[cfg(process_any)] mod process; +#[cfg(shell_any)] mod shell; mod window; @@ -62,18 +72,28 @@ impl From for InvokeResponse { #[serde(tag = "module", content = "message")] enum Module { App(app::Cmd), + #[cfg(process_any)] Process(process::Cmd), + #[cfg(fs_any)] Fs(file_system::Cmd), + #[cfg(os_any)] Os(operating_system::Cmd), + #[cfg(path_any)] Path(path::Cmd), Window(Box), + #[cfg(shell_any)] Shell(shell::Cmd), Event(event::Cmd), + #[cfg(dialog_any)] Dialog(dialog::Cmd), + #[cfg(cli)] Cli(cli::Cmd), Notification(notification::Cmd), + #[cfg(http_any)] Http(http::Cmd), + #[cfg(global_shortcut_any)] GlobalShortcut(global_shortcut::Cmd), + #[cfg(clipboard_any)] Clipboard(clipboard::Cmd), } @@ -97,24 +117,28 @@ impl Module { .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(process_any)] Self::Process(cmd) => resolver.respond_async(async move { cmd .run(context) .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(fs_any)] Self::Fs(cmd) => resolver.respond_async(async move { cmd .run(context) .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(path_any)] Self::Path(cmd) => resolver.respond_async(async move { cmd .run(context) .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(os_any)] Self::Os(cmd) => resolver.respond_async(async move { cmd .run(context) @@ -128,6 +152,7 @@ impl Module { .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(shell_any)] Self::Shell(cmd) => resolver.respond_async(async move { cmd .run(context) @@ -140,12 +165,14 @@ impl Module { .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(dialog_any)] Self::Dialog(cmd) => resolver.respond_async(async move { cmd .run(context) .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(cli)] Self::Cli(cmd) => resolver.respond_async(async move { cmd .run(context) @@ -158,6 +185,7 @@ impl Module { .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(http_any)] Self::Http(cmd) => resolver.respond_async(async move { cmd .run(context) @@ -165,12 +193,14 @@ impl Module { .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(global_shortcut_any)] Self::GlobalShortcut(cmd) => resolver.respond_async(async move { cmd .run(context) .and_then(|r| r.json) .map_err(InvokeError::from_anyhow) }), + #[cfg(clipboard_any)] Self::Clipboard(cmd) => resolver.respond_async(async move { cmd .run(context) @@ -195,11 +225,28 @@ pub(crate) fn handle( } = message; if let JsonValue::Object(ref mut obj) = payload { - obj.insert("module".to_string(), JsonValue::String(module)); + obj.insert("module".to_string(), JsonValue::String(module.clone())); } match serde_json::from_value::(payload) { Ok(module) => module.run(window, resolver, config, package_info.clone()), - Err(e) => resolver.reject(e.to_string()), + Err(e) => { + let message = e.to_string(); + if message.starts_with("unknown variant") { + let mut s = message.split('`'); + s.next(); + if let Some(unknown_variant_name) = s.next() { + if unknown_variant_name == module { + return resolver.reject(format!( + "The `{}` module is not enabled. You must enable one of its APIs in the allowlist.", + module + )); + } else if module == "Window" { + return resolver.reject(window::into_allowlist_error(unknown_variant_name).to_string()); + } + } + } + resolver.reject(message); + } } } diff --git a/core/tauri/src/endpoints/window.rs b/core/tauri/src/endpoints/window.rs index 85e69d689..0fdadeb94 100644 --- a/core/tauri/src/endpoints/window.rs +++ b/core/tauri/src/endpoints/window.rs @@ -72,38 +72,67 @@ pub enum WindowManagerCmd { AvailableMonitors, Theme, // Setters + #[cfg(window_center)] Center, + #[cfg(window_request_user_attention)] RequestUserAttention(Option), + #[cfg(window_set_resizable)] SetResizable(bool), + #[cfg(window_set_title)] SetTitle(String), + #[cfg(window_maximize)] Maximize, + #[cfg(window_unmaximize)] Unmaximize, + #[cfg(all(window_maximize, window_unmaximize))] ToggleMaximize, + #[cfg(window_minimize)] Minimize, + #[cfg(window_unminimize)] Unminimize, + #[cfg(window_show)] Show, + #[cfg(window_hide)] Hide, + #[cfg(window_close)] Close, + #[cfg(window_set_decorations)] SetDecorations(bool), + #[cfg(window_set_always_on_top)] #[serde(rename_all = "camelCase")] SetAlwaysOnTop(bool), + #[cfg(window_set_size)] SetSize(Size), + #[cfg(window_set_min_size)] SetMinSize(Option), + #[cfg(window_set_max_size)] SetMaxSize(Option), + #[cfg(window_set_position)] SetPosition(Position), + #[cfg(window_set_fullscreen)] SetFullscreen(bool), + #[cfg(window_set_focus)] SetFocus, + #[cfg(window_set_icon)] SetIcon { icon: IconDto, }, + #[cfg(window_set_skip_taskbar)] SetSkipTaskbar(bool), + #[cfg(window_set_cursor_grab)] SetCursorGrab(bool), + #[cfg(window_set_cursor_visible)] SetCursorVisible(bool), + #[cfg(window_set_cursor_icon)] SetCursorIcon(CursorIcon), + #[cfg(window_set_cursor_position)] SetCursorPosition(Position), + #[cfg(window_start_dragging)] StartDragging, + #[cfg(window_print)] Print, // internals + #[cfg(all(window_maximize, window_unmaximize))] #[serde(rename = "__toggleMaximize")] InternalToggleMaximize, #[cfg(any(debug_assertions, feature = "devtools"))] @@ -111,61 +140,45 @@ pub enum WindowManagerCmd { InternalToggleDevtools, } -impl WindowManagerCmd { - fn into_allowlist_error(self) -> crate::Error { - match self { - Self::Center => crate::Error::ApiNotAllowlisted("window > center".to_string()), - Self::RequestUserAttention(_) => { - crate::Error::ApiNotAllowlisted("window > requestUserAttention".to_string()) - } - Self::SetResizable(_) => crate::Error::ApiNotAllowlisted("window > setResizable".to_string()), - Self::SetTitle(_) => crate::Error::ApiNotAllowlisted("window > setTitle".to_string()), - Self::Maximize => crate::Error::ApiNotAllowlisted("window > maximize".to_string()), - Self::Unmaximize => crate::Error::ApiNotAllowlisted("window > unmaximize".to_string()), - Self::ToggleMaximize => { - crate::Error::ApiNotAllowlisted("window > maximize and window > unmaximize".to_string()) - } - Self::Minimize => crate::Error::ApiNotAllowlisted("window > minimize".to_string()), - Self::Unminimize => crate::Error::ApiNotAllowlisted("window > unminimize".to_string()), - Self::Show => crate::Error::ApiNotAllowlisted("window > show".to_string()), - Self::Hide => crate::Error::ApiNotAllowlisted("window > hide".to_string()), - Self::Close => crate::Error::ApiNotAllowlisted("window > close".to_string()), - Self::SetDecorations(_) => { - crate::Error::ApiNotAllowlisted("window > setDecorations".to_string()) - } - Self::SetAlwaysOnTop(_) => { - crate::Error::ApiNotAllowlisted("window > setAlwaysOnTop".to_string()) - } - Self::SetSize(_) => crate::Error::ApiNotAllowlisted("window > setSize".to_string()), - Self::SetMinSize(_) => crate::Error::ApiNotAllowlisted("window > setMinSize".to_string()), - Self::SetMaxSize(_) => crate::Error::ApiNotAllowlisted("window > setMaxSize".to_string()), - Self::SetPosition(_) => crate::Error::ApiNotAllowlisted("window > setPosition".to_string()), - Self::SetFullscreen(_) => { - crate::Error::ApiNotAllowlisted("window > setFullscreen".to_string()) - } - Self::SetIcon { .. } => crate::Error::ApiNotAllowlisted("window > setIcon".to_string()), - Self::SetSkipTaskbar(_) => { - crate::Error::ApiNotAllowlisted("window > setSkipTaskbar".to_string()) - } - Self::SetCursorGrab(_) => { - crate::Error::ApiNotAllowlisted("window > setCursorGrab".to_string()) - } - Self::SetCursorVisible(_) => { - crate::Error::ApiNotAllowlisted("window > setCursorVisible".to_string()) - } - Self::SetCursorIcon(_) => { - crate::Error::ApiNotAllowlisted("window > setCursorIcon".to_string()) - } - Self::SetCursorPosition(_) => { - crate::Error::ApiNotAllowlisted("window > setCursorPosition".to_string()) - } - Self::StartDragging => crate::Error::ApiNotAllowlisted("window > startDragging".to_string()), - Self::Print => crate::Error::ApiNotAllowlisted("window > print".to_string()), - Self::InternalToggleMaximize => { - crate::Error::ApiNotAllowlisted("window > maximize and window > unmaximize".to_string()) - } - _ => crate::Error::ApiNotAllowlisted("window > all".to_string()), +pub fn into_allowlist_error(variant: &str) -> crate::Error { + match variant { + "center" => crate::Error::ApiNotAllowlisted("window > center".to_string()), + "requestUserAttention" => { + crate::Error::ApiNotAllowlisted("window > requestUserAttention".to_string()) } + "setResizable" => crate::Error::ApiNotAllowlisted("window > setResizable".to_string()), + "setTitle" => crate::Error::ApiNotAllowlisted("window > setTitle".to_string()), + "maximize" => crate::Error::ApiNotAllowlisted("window > maximize".to_string()), + "unmaximize" => crate::Error::ApiNotAllowlisted("window > unmaximize".to_string()), + "toggleMaximize" => { + crate::Error::ApiNotAllowlisted("window > maximize and window > unmaximize".to_string()) + } + "minimize" => crate::Error::ApiNotAllowlisted("window > minimize".to_string()), + "nnminimize" => crate::Error::ApiNotAllowlisted("window > unminimize".to_string()), + "show" => crate::Error::ApiNotAllowlisted("window > show".to_string()), + "hide" => crate::Error::ApiNotAllowlisted("window > hide".to_string()), + "close" => crate::Error::ApiNotAllowlisted("window > close".to_string()), + "setDecorations" => crate::Error::ApiNotAllowlisted("window > setDecorations".to_string()), + "setAlwaysOnTop" => crate::Error::ApiNotAllowlisted("window > setAlwaysOnTop".to_string()), + "setSize" => crate::Error::ApiNotAllowlisted("window > setSize".to_string()), + "setMinSize" => crate::Error::ApiNotAllowlisted("window > setMinSize".to_string()), + "setMaxSize" => crate::Error::ApiNotAllowlisted("window > setMaxSize".to_string()), + "setPosition" => crate::Error::ApiNotAllowlisted("window > setPosition".to_string()), + "setFullscreen" => crate::Error::ApiNotAllowlisted("window > setFullscreen".to_string()), + "setIcon" => crate::Error::ApiNotAllowlisted("window > setIcon".to_string()), + "setSkipTaskbar" => crate::Error::ApiNotAllowlisted("window > setSkipTaskbar".to_string()), + "setCursorGrab" => crate::Error::ApiNotAllowlisted("window > setCursorGrab".to_string()), + "setCursorVisible" => crate::Error::ApiNotAllowlisted("window > setCursorVisible".to_string()), + "setCursorIcon" => crate::Error::ApiNotAllowlisted("window > setCursorIcon".to_string()), + "setCursorPosition" => { + crate::Error::ApiNotAllowlisted("window > setCursorPosition".to_string()) + } + "startDragging" => crate::Error::ApiNotAllowlisted("window > startDragging".to_string()), + "print" => crate::Error::ApiNotAllowlisted("window > print".to_string()), + "internalToggleMaximize" => { + crate::Error::ApiNotAllowlisted("window > maximize and window > unmaximize".to_string()) + } + _ => crate::Error::ApiNotAllowlisted("window".to_string()), } } @@ -318,8 +331,6 @@ impl Cmd { window.open_devtools(); } } - #[allow(unreachable_patterns)] - _ => return Err(cmd.into_allowlist_error()), } #[allow(unreachable_code)] Ok(().into()) diff --git a/core/tauri/src/lib.rs b/core/tauri/src/lib.rs index 8d8ca25f0..e8731edf2 100644 --- a/core/tauri/src/lib.rs +++ b/core/tauri/src/lib.rs @@ -803,23 +803,100 @@ pub mod test; #[cfg(test)] mod tests { + use cargo_toml::Manifest; + use once_cell::sync::OnceCell; + use std::{env::var, fs::read_to_string, path::PathBuf}; + + static MANIFEST: OnceCell = OnceCell::new(); + const CHECKED_FEATURES: &str = include_str!(concat!(env!("OUT_DIR"), "/checked_features")); + + fn get_manifest() -> &'static Manifest { + MANIFEST.get_or_init(|| { + let manifest_dir = PathBuf::from(var("CARGO_MANIFEST_DIR").unwrap()); + let manifest = Manifest::from_path(manifest_dir.join("Cargo.toml")) + .expect("failed to parse Cargo manifest"); + manifest + }) + } + #[test] fn features_are_documented() { - use cargo_toml::Manifest; - use std::{env::var, fs::read_to_string, path::PathBuf}; - // this env var is always set by Cargo let manifest_dir = PathBuf::from(var("CARGO_MANIFEST_DIR").unwrap()); - let manifest = - Manifest::from_path(manifest_dir.join("Cargo.toml")).expect("failed to parse Cargo manifest"); - let lib_code = read_to_string(manifest_dir.join("src/lib.rs")).expect("failed to read lib.rs"); - for (f, _) in manifest.features { + for (f, _) in &get_manifest().features { if !(f.starts_with("__") || f == "default" || lib_code.contains(&format!("*{}**", f))) { panic!("Feature {} is not documented", f); } } } + + #[test] + fn aliased_features_exist() { + let checked_features = CHECKED_FEATURES.split(','); + let manifest = get_manifest(); + for checked_feature in checked_features { + if !manifest.features.iter().any(|(f, _)| f == checked_feature) { + panic!( + "Feature {} was checked in the alias build step but it does not exist in core/tauri/Cargo.toml", + checked_feature + ); + } + } + } + + #[test] + fn all_allowlist_features_are_aliased() { + let manifest = get_manifest(); + let all_modules = manifest + .features + .iter() + .find(|(f, _)| f.as_str() == "api-all") + .map(|(_, enabled)| enabled) + .expect("api-all feature must exist"); + + let checked_features = CHECKED_FEATURES.split(',').collect::>(); + assert!( + checked_features.contains(&"api-all"), + "`api-all` is not aliased" + ); + + // features that look like an allowlist feature, but are not + let allowed = [ + "fs-extract-api", + "http-api", + "http-multipart", + "process-command-api", + "process-relaunch-dangerous-allow-symlink-macos", + "window-data-url", + ]; + + for module_all_feature in all_modules { + let module = module_all_feature.replace("-all", ""); + assert!( + checked_features.contains(&module_all_feature.as_str()), + "`{}` is not aliased", + module + ); + + let module_prefix = format!("{}-", module); + // we assume that module features are the ones that start with `-` + // though it's not 100% accurate, we have an allowed list to fix it + let module_features = manifest + .features + .iter() + .map(|(f, _)| f) + .filter(|f| f.starts_with(&module_prefix)); + for module_feature in module_features { + assert!( + allowed.contains(&module_feature.as_str()) + || checked_features.contains(&module_feature.as_str()), + "`{}` is not aliased", + module_feature + ); + } + } + } } #[cfg(test)] diff --git a/core/tauri/src/window.rs b/core/tauri/src/window.rs index 3931e72c8..86f313f06 100644 --- a/core/tauri/src/window.rs +++ b/core/tauri/src/window.rs @@ -605,8 +605,12 @@ impl Window { let invoke = Invoke { message, resolver }; if let Some(module) = &payload.tauri_module { - let module = module.to_string(); - crate::endpoints::handle(module, invoke, manager.config(), manager.package_info()); + crate::endpoints::handle( + module.to_string(), + invoke, + manager.config(), + manager.package_info(), + ); } else if payload.cmd.starts_with("plugin:") { manager.extend_api(invoke); } else {