refactor: move deleted tauri APIs, prepare for next release (#355)

pull/357/head
Lucas Fernandes Nogueira 2 years ago committed by GitHub
parent 937e6a5be6
commit 702b7b36bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -71,7 +71,8 @@
"dialog": {
"path": "./plugins/dialog",
"manager": "rust-disabled"
"manager": "rust-disabled",
"dependencies": ["fs"]
},
"dialog-js": {
"path": "./plugins/dialog",
@ -107,7 +108,8 @@
"http": {
"path": "./plugins/http",
"manager": "rust-disabled"
"manager": "rust-disabled",
"dependencies": ["fs"]
},
"http-js": {
"path": "./plugins/http",
@ -139,7 +141,8 @@
"persisted-scope": {
"path": "./plugins/persisted-scope",
"manager": "rust"
"manager": "rust",
"dependencies": ["fs"]
},
"positioner": {

31
Cargo.lock generated

@ -4960,15 +4960,13 @@ dependencies = [
[[package]]
name = "tauri"
version = "2.0.0-alpha.8"
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
dependencies = [
"anyhow",
"bytes 1.4.0",
"cocoa",
"dirs-next",
"embed_plist",
"encoding_rs",
"flate2",
"futures-util",
"glib",
"glob",
@ -4976,7 +4974,6 @@ dependencies = [
"heck 0.4.1",
"http",
"ico 0.2.0",
"ignore",
"infer 0.9.0",
"jni",
"libc",
@ -4995,7 +4992,6 @@ dependencies = [
"serialize-to-javascript",
"state",
"swift-rs",
"tar",
"tauri-build",
"tauri-macros",
"tauri-runtime",
@ -5003,24 +4999,21 @@ dependencies = [
"tauri-utils",
"tempfile",
"thiserror",
"time 0.3.20",
"tokio",
"url",
"uuid",
"webkit2gtk",
"webview2-com",
"windows 0.44.0",
"zip",
]
[[package]]
name = "tauri-build"
version = "2.0.0-alpha.4"
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
dependencies = [
"anyhow",
"cargo_toml",
"filetime",
"heck 0.4.1",
"json-patch",
"quote",
@ -5037,7 +5030,7 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.0.0-alpha.4"
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
dependencies = [
"base64 0.21.0",
"brotli",
@ -5062,7 +5055,7 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.0.0-alpha.4"
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@ -5147,6 +5140,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-fs",
"thiserror",
]
@ -5155,9 +5149,11 @@ name = "tauri-plugin-fs"
version = "0.0.0"
dependencies = [
"anyhow",
"glob",
"serde",
"tauri",
"thiserror",
"uuid",
]
[[package]]
@ -5198,6 +5194,7 @@ dependencies = [
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin-fs",
"thiserror",
]
@ -5273,6 +5270,7 @@ dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-plugin-fs",
"thiserror",
]
@ -5373,8 +5371,10 @@ version = "0.0.0"
dependencies = [
"base64 0.21.0",
"dirs-next",
"flate2",
"futures-util",
"http",
"ignore",
"minisign-verify",
"mockito",
"percent-encoding",
@ -5382,6 +5382,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tempfile",
"thiserror",
@ -5389,6 +5390,7 @@ dependencies = [
"tokio",
"tokio-test",
"url",
"zip",
]
[[package]]
@ -5447,7 +5449,7 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.13.0-alpha.4"
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
dependencies = [
"gtk",
"http",
@ -5461,14 +5463,13 @@ dependencies = [
"thiserror",
"url",
"uuid",
"webview2-com",
"windows 0.44.0",
]
[[package]]
name = "tauri-runtime-wry"
version = "0.13.0-alpha.4"
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
dependencies = [
"cocoa",
"gtk",
@ -5488,7 +5489,7 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.0.0-alpha.4"
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
dependencies = [
"aes-gcm 0.10.1",
"brotli",

File diff suppressed because it is too large Load Diff

@ -33,13 +33,12 @@ tauri-plugin-window = { path = "../../../plugins/window" }
[dependencies.tauri]
workspace = true
features = [
"api-all",
"icon-ico",
"icon-png",
"isolation",
"macos-private-api",
"system-tray",
"updater"
"protocol-asset"
]
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]

@ -46,47 +46,8 @@
]
}
}
}
},
"tauri": {
"pattern": {
"use": "isolation",
"options": {
"dir": "../isolation-dist/"
}
},
"macOSPrivateApi": true,
"bundle": {
"active": true,
"identifier": "com.tauri.api",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"wix": {
"language": {
"en-US": {},
"pt-BR": {
"localePath": "locales/pt-BR.wxl"
}
}
}
}
},
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
"endpoints": [
"https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}"
]
},
"allowlist": {
"all": true,
"fs": {
"fs": {
"scope": {
"allow": ["$APPDATA/db/**", "$DOWNLOAD/**", "$RESOURCE/**"],
"deny": ["$APPDATA/db/*.stronghold"]
@ -117,15 +78,46 @@
}
]
},
"protocol": {
"asset": true,
"assetScope": {
"allow": ["$APPDATA/db/**", "$RESOURCE/**"],
"deny": ["$APPDATA/db/*.stronghold"]
}
},
"http": {
"scope": ["http://localhost:3003"]
},
"updater": {
"endpoints": [
"https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}"
]
}
},
"tauri": {
"pattern": {
"use": "isolation",
"options": {
"dir": "../isolation-dist/"
}
},
"macOSPrivateApi": true,
"bundle": {
"active": true,
"identifier": "com.tauri.api",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"wix": {
"language": {
"en-US": {},
"pt-BR": {
"localePath": "locales/pt-BR.wxl"
}
}
}
},
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK"
}
},
"windows": [],
@ -136,7 +128,14 @@
"img-src": "'self' asset: https://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
},
"freezePrototype": true
"freezePrototype": true,
"assetProtocol": {
"enable": true,
"scope": {
"allow": ["$APPDATA/db/**", "$RESOURCE/**"],
"deny": ["$APPDATA/db/*.stronghold"]
}
}
},
"systemTray": {
"iconPath": "icons/tray_icon_with_transparency.png",

@ -12,8 +12,7 @@ pub fn name<R: Runtime>(app: AppHandle<R>) -> String {
#[tauri::command]
pub fn tauri_version() -> &'static str {
// TODO: return actual tauri version with `tauri::VERSION`
env!("CARGO_PKG_VERSION")
tauri::VERSION
}
#[tauri::command]

@ -11,12 +11,11 @@ use config::{Arg, Config};
pub use error::Error;
type Result<T> = std::result::Result<T, Error>;
// TODO: use PluginApi#app when 2.0.0-alpha.9 is released
pub struct Cli<R: Runtime>(PluginApi<R, Config>, AppHandle<R>);
pub struct Cli<R: Runtime>(PluginApi<R, Config>);
impl<R: Runtime> Cli<R> {
pub fn matches(&self) -> Result<parser::Matches> {
parser::get_matches(self.0.config(), self.1.package_info())
parser::get_matches(self.0.config(), self.0.app().package_info())
}
}
@ -39,7 +38,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
Builder::new("cli")
.invoke_handler(tauri::generate_handler![cli_matches])
.setup(|app, api| {
app.manage(Cli(api, app.clone()));
app.manage(Cli(api));
Ok(())
})
.build()

File diff suppressed because it is too large Load Diff

@ -13,6 +13,7 @@ serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
tauri-plugin-fs = { path = "../fs", version = "0.0.0" }
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
glib = "0.16"

@ -6,6 +6,7 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tauri::{command, Manager, Runtime, State, Window};
use tauri_plugin_fs::FsExt;
use crate::{Dialog, FileDialogBuilder, FileResponse, MessageDialogKind, Result};
@ -114,16 +115,18 @@ pub(crate) async fn open<R: Runtime>(
let folders = dialog_builder.blocking_pick_folders();
if let Some(folders) = &folders {
for folder in folders {
window
.fs_scope()
.allow_directory(folder, options.recursive)?;
if let Some(s) = window.try_fs_scope() {
s.allow_directory(folder, options.recursive)?;
}
}
}
OpenResponse::Folders(folders)
} else {
let folder = dialog_builder.blocking_pick_folder();
if let Some(path) = &folder {
window.fs_scope().allow_directory(path, options.recursive)?;
if let Some(s) = window.try_fs_scope() {
s.allow_directory(path, options.recursive)?;
}
}
OpenResponse::Folder(folder)
}
@ -134,14 +137,24 @@ pub(crate) async fn open<R: Runtime>(
let files = dialog_builder.blocking_pick_files();
if let Some(files) = &files {
for file in files {
window.fs_scope().allow_file(&file.path)?;
if let Some(s) = window.try_fs_scope() {
s.allow_file(&file.path)?;
}
window
.state::<tauri::scope::Scopes>()
.allow_file(&file.path)?;
}
}
OpenResponse::Files(files)
} else {
let file = dialog_builder.blocking_pick_file();
if let Some(file) = &file {
window.fs_scope().allow_file(&file.path)?;
if let Some(s) = window.try_fs_scope() {
s.allow_file(&file.path)?;
}
window
.state::<tauri::scope::Scopes>()
.allow_file(&file.path)?;
}
OpenResponse::File(file)
};
@ -177,7 +190,10 @@ pub(crate) async fn save<R: Runtime>(
let path = dialog_builder.blocking_save_file();
if let Some(p) = &path {
window.fs_scope().allow_file(p)?;
if let Some(s) = window.try_fs_scope() {
s.allow_file(p)?;
}
window.state::<tauri::scope::Scopes>().allow_file(p)?;
}
Ok(path)

@ -10,6 +10,7 @@
use std::path::PathBuf;
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
@ -101,6 +102,14 @@ impl From<MessageDialogKind> for rfd::MessageLevel {
}
}
struct WindowHandle(RawWindowHandle);
unsafe impl HasRawWindowHandle for WindowHandle {
fn raw_window_handle(&self) -> RawWindowHandle {
self.0
}
}
impl<R: Runtime> From<FileDialogBuilder<R>> for FileDialog {
fn from(d: FileDialogBuilder<R>) -> Self {
let mut builder = FileDialog::new();
@ -119,8 +128,8 @@ impl<R: Runtime> From<FileDialogBuilder<R>> for FileDialog {
builder = builder.add_filter(&filter.name, &v);
}
#[cfg(desktop)]
if let Some(_parent) = d.parent {
// TODO builder = builder.set_parent(&parent);
if let Some(parent) = d.parent {
builder = builder.set_parent(&WindowHandle(parent));
}
builder
@ -144,8 +153,8 @@ impl<R: Runtime> From<MessageDialogBuilder<R>> for rfd::MessageDialog {
dialog = dialog.set_buttons(buttons);
}
if let Some(_parent) = d.parent {
// TODO dialog.set_parent(parent);
if let Some(parent) = d.parent {
dialog = dialog.set_parent(&WindowHandle(parent));
}
dialog

@ -21,6 +21,8 @@ pub enum Error {
#[cfg(mobile)]
#[error("File save dialog is not implemented on mobile")]
FileSaveDialogNotImplemented,
#[error(transparent)]
Fs(#[from] tauri_plugin_fs::Error),
}
impl Serialize for Error {

3591
plugins/fs/Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -14,3 +14,5 @@ serde.workspace = true
tauri.workspace = true
thiserror.workspace = true
anyhow = "1"
uuid = { version = "1", features = ["v4"] }
glob = "0.3"

@ -1,8 +1,9 @@
use crate::Scope;
use anyhow::Context;
use serde::{Deserialize, Serialize, Serializer};
use tauri::{
path::{BaseDirectory, SafePathBuf},
FsScope, Manager, Runtime, Window,
Manager, Runtime, Window,
};
#[cfg(unix)]
@ -16,7 +17,7 @@ use std::{
time::{SystemTime, UNIX_EPOCH},
};
use crate::{Error, Result};
use crate::{Error, FsExt, Result};
#[derive(Debug, thiserror::Error)]
pub enum CommandError {
@ -120,7 +121,7 @@ pub fn write_file<R: Runtime>(
#[derive(Clone, Copy)]
struct ReadDirOptions<'a> {
pub scope: Option<&'a FsScope>,
pub scope: Option<&'a Scope>,
}
#[derive(Debug, Serialize)]
@ -189,7 +190,7 @@ pub fn read_dir<R: Runtime>(
&resolved_path,
recursive,
ReadDirOptions {
scope: Some(&window.fs_scope()),
scope: Some(window.fs_scope()),
},
)
.with_context(|| format!("path: {}", resolved_path.display()))

@ -0,0 +1,57 @@
use std::path::PathBuf;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Config {
pub scope: FsScope,
}
/// Protocol scope definition.
/// It is a list of glob patterns that restrict the API access from the webview.
///
/// Each pattern can start with a variable that resolves to a system base directory.
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged)]
pub enum FsScope {
/// A list of paths that are allowed by this scope.
AllowedPaths(Vec<PathBuf>),
/// A complete scope configuration.
Scope {
/// A list of paths that are allowed by this scope.
#[serde(default)]
allow: Vec<PathBuf>,
/// A list of paths that are not allowed by this scope.
/// This gets precedence over the [`Self::Scope::allow`] list.
#[serde(default)]
deny: Vec<PathBuf>,
},
}
impl Default for FsScope {
fn default() -> Self {
Self::AllowedPaths(Vec::new())
}
}
impl FsScope {
/// The list of allowed paths.
pub fn allowed_paths(&self) -> &Vec<PathBuf> {
match self {
Self::AllowedPaths(p) => p,
Self::Scope { allow, .. } => allow,
}
}
/// The list of forbidden paths.
pub fn forbidden_paths(&self) -> Option<&Vec<PathBuf>> {
match self {
Self::AllowedPaths(_) => None,
Self::Scope { deny, .. } => Some(deny),
}
}
}

@ -10,6 +10,9 @@ pub enum Error {
PathForbidden(PathBuf),
#[error("failed to resolve path: {0}")]
CannotResolvePath(tauri::path::Error),
/// Invalid glob pattern.
#[error("invalid glob pattern: {0}")]
GlobPattern(#[from] glob::PatternError),
}
impl Serialize for Error {

@ -2,20 +2,40 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use config::FsScope;
use tauri::{
plugin::{Builder as PluginBuilder, TauriPlugin},
Runtime,
FileDropEvent, Manager, RunEvent, Runtime, WindowEvent,
};
mod commands;
mod config;
mod error;
mod scope;
pub use config::Config;
pub use error::Error;
pub use scope::{Event as ScopeEvent, Scope};
type Result<T> = std::result::Result<T, Error>;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new("fs")
pub trait FsExt<R: Runtime> {
fn fs_scope(&self) -> &Scope;
fn try_fs_scope(&self) -> Option<&Scope>;
}
impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
fn fs_scope(&self) -> &Scope {
self.state::<Scope>().inner()
}
fn try_fs_scope(&self) -> Option<&Scope> {
self.try_state::<Scope>().map(|s| s.inner())
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
PluginBuilder::<R, Option<Config>>::new("fs")
.invoke_handler(tauri::generate_handler![
commands::read_file,
commands::read_text_file,
@ -29,5 +49,33 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::exists,
commands::metadata
])
.setup(|app: &tauri::AppHandle<R>, api| {
let default_scope = FsScope::default();
app.manage(Scope::new(
app,
api.config()
.as_ref()
.map(|c| &c.scope)
.unwrap_or(&default_scope),
)?);
Ok(())
})
.on_event(|app, event| {
if let RunEvent::WindowEvent {
label: _,
event: WindowEvent::FileDrop(FileDropEvent::Dropped(paths)),
..
} = event
{
let scope = app.fs_scope();
for path in paths {
if path.is_file() {
let _ = scope.allow_file(path);
} else {
let _ = scope.allow_directory(path, false);
}
}
}
})
.build()
}

@ -0,0 +1,368 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
collections::{HashMap, HashSet},
fmt,
path::{Path, PathBuf, MAIN_SEPARATOR},
sync::{Arc, Mutex},
};
use crate::config::FsScope;
pub use glob::Pattern;
use uuid::Uuid;
use crate::{Manager, Runtime};
/// Scope change event.
#[derive(Debug, Clone)]
pub enum Event {
/// A path has been allowed.
PathAllowed(PathBuf),
/// A path has been forbidden.
PathForbidden(PathBuf),
}
type EventListener = Box<dyn Fn(&Event) + Send>;
/// Scope for filesystem access.
#[derive(Clone)]
pub struct Scope {
allowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
event_listeners: Arc<Mutex<HashMap<Uuid, EventListener>>>,
}
impl fmt::Debug for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Scope")
.field(
"allowed_patterns",
&self
.allowed_patterns
.lock()
.unwrap()
.iter()
.map(|p| p.as_str())
.collect::<Vec<&str>>(),
)
.field(
"forbidden_patterns",
&self
.forbidden_patterns
.lock()
.unwrap()
.iter()
.map(|p| p.as_str())
.collect::<Vec<&str>>(),
)
.finish()
}
}
fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternError>>(
list: &mut HashSet<Pattern>,
pattern: P,
f: F,
) -> crate::Result<()> {
let path: PathBuf = pattern.as_ref().components().collect();
list.insert(f(&path.to_string_lossy())?);
#[cfg(windows)]
{
if let Ok(p) = std::fs::canonicalize(&path) {
list.insert(f(&p.to_string_lossy())?);
} else {
list.insert(f(&format!("\\\\?\\{}", path.display()))?);
}
}
Ok(())
}
impl Scope {
/// Creates a new scope from a `FsAllowlistScope` configuration.
pub(crate) fn new<R: Runtime, M: Manager<R>>(
manager: &M,
scope: &FsScope,
) -> crate::Result<Self> {
let mut allowed_patterns = HashSet::new();
for path in scope.allowed_paths() {
if let Ok(path) = manager.path().parse(path) {
push_pattern(&mut allowed_patterns, path, Pattern::new)?;
}
}
let mut forbidden_patterns = HashSet::new();
if let Some(forbidden_paths) = scope.forbidden_paths() {
for path in forbidden_paths {
if let Ok(path) = manager.path().parse(path) {
push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
}
}
}
Ok(Self {
allowed_patterns: Arc::new(Mutex::new(allowed_patterns)),
forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
event_listeners: Default::default(),
})
}
/// The list of allowed patterns.
pub fn allowed_patterns(&self) -> HashSet<Pattern> {
self.allowed_patterns.lock().unwrap().clone()
}
/// The list of forbidden patterns.
pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
self.forbidden_patterns.lock().unwrap().clone()
}
/// Listen to an event on this scope.
pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> Uuid {
let id = Uuid::new_v4();
self.event_listeners.lock().unwrap().insert(id, Box::new(f));
id
}
fn trigger(&self, event: Event) {
let listeners = self.event_listeners.lock().unwrap();
let handlers = listeners.values();
for listener in handlers {
listener(&event);
}
}
/// Extend the allowed patterns with the given directory.
///
/// After this function has been called, the frontend will be able to use the Tauri API to read
/// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too.
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref();
{
let mut list = self.allowed_patterns.lock().unwrap();
// allow the directory to be read
push_pattern(&mut list, path, escaped_pattern)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
self.trigger(Event::PathAllowed(path.to_path_buf()));
Ok(())
}
/// Extend the allowed patterns with the given file path.
///
/// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
let path = path.as_ref();
push_pattern(
&mut self.allowed_patterns.lock().unwrap(),
path,
escaped_pattern,
)?;
self.trigger(Event::PathAllowed(path.to_path_buf()));
Ok(())
}
/// Set the given directory path to be forbidden by this scope.
///
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref();
{
let mut list = self.forbidden_patterns.lock().unwrap();
// allow the directory to be read
push_pattern(&mut list, path, escaped_pattern)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
self.trigger(Event::PathForbidden(path.to_path_buf()));
Ok(())
}
/// Set the given file path to be forbidden by this scope.
///
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
let path = path.as_ref();
push_pattern(
&mut self.forbidden_patterns.lock().unwrap(),
path,
escaped_pattern,
)?;
self.trigger(Event::PathForbidden(path.to_path_buf()));
Ok(())
}
/// Determines if the given path is allowed on this scope.
pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
let path = path.as_ref();
let path = if !path.exists() {
crate::Result::Ok(path.to_path_buf())
} else {
std::fs::canonicalize(path).map_err(Into::into)
};
if let Ok(path) = path {
let path: PathBuf = path.components().collect();
let options = glob::MatchOptions {
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
// see: https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5
require_literal_separator: true,
// dotfiles are not supposed to be exposed by default
#[cfg(unix)]
require_literal_leading_dot: true,
..Default::default()
};
let forbidden = self
.forbidden_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path_with(&path, options));
if forbidden {
false
} else {
let allowed = self
.allowed_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path_with(&path, options));
allowed
}
} else {
false
}
}
}
fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
Pattern::new(&glob::Pattern::escape(p))
}
fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
Pattern::new(&format!(
"{}{}{append}",
glob::Pattern::escape(p),
MAIN_SEPARATOR
))
}
#[cfg(test)]
mod tests {
use super::Scope;
fn new_scope() -> Scope {
Scope {
allowed_patterns: Default::default(),
forbidden_patterns: Default::default(),
event_listeners: Default::default(),
}
}
#[test]
fn path_is_escaped() {
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri/**", false).unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_file("/home/tauri/**").unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_file("C:\\home\\tauri\\**").unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_directory("/home/tauri/**", false).unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", true).unwrap();
scope
.forbid_directory("C:\\home\\tauri\\**", false)
.unwrap();
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_file("/home/tauri/**").unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", true).unwrap();
scope.forbid_file("C:\\home\\tauri\\**").unwrap();
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", false).unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", false).unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
}
}

3874
plugins/http/Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -10,6 +10,7 @@ serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
thiserror.workspace = true
tauri-plugin-fs = { path = "../fs", version = "0.0.0" }
glob = "0.3"
rand = "0.8"
bytes = { version = "1", features = [ "serde" ] }

@ -1,4 +1,5 @@
use tauri::{path::SafePathBuf, AppHandle, Runtime, State};
use tauri_plugin_fs::FsExt;
use crate::{ClientId, Http};
@ -38,7 +39,6 @@ pub async fn request<R: Runtime>(
client_id: ClientId,
options: Box<HttpRequestBuilder>,
) -> super::Result<ResponseData> {
use crate::Manager;
if http.scope.is_allowed(&options.url) {
let client = http
.clients
@ -55,7 +55,12 @@ pub async fn request<R: Runtime>(
..
} = value
{
if SafePathBuf::new(path.clone()).is_err() || !app.fs_scope().is_allowed(path) {
if SafePathBuf::new(path.clone()).is_err()
|| !app
.try_fs_scope()
.map(|s| s.is_allowed(path))
.unwrap_or_default()
{
return Err(crate::Error::PathNotAllowed(path.clone()));
}
}

@ -0,0 +1,19 @@
use reqwest::Url;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Config {
pub scope: HttpAllowlistScope,
}
/// HTTP API scope definition.
/// It is a list of URLs that can be accessed by the webview when using the HTTP APIs.
/// The scoped URL is matched against the request URL using a glob pattern.
///
/// Examples:
/// - "https://**": allows all HTTPS urls
/// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path
/// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/"
#[allow(rustdoc::bare_urls)]
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
pub struct HttpAllowlistScope(pub Vec<Url>);

@ -1,3 +1,4 @@
use config::{Config, HttpAllowlistScope};
pub use reqwest as client;
use tauri::{
plugin::{Builder, TauriPlugin},
@ -7,6 +8,7 @@ use tauri::{
use std::{collections::HashMap, sync::Mutex};
mod commands;
mod config;
mod error;
mod scope;
@ -33,18 +35,24 @@ impl<R: Runtime, T: Manager<R>> HttpExt<R> for T {
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("http")
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
Builder::<R, Option<Config>>::new("http")
.invoke_handler(tauri::generate_handler![
commands::create_client,
commands::drop_client,
commands::request
])
.setup(|app, _api| {
.setup(|app, api| {
let default_scope = HttpAllowlistScope::default();
app.manage(Http {
app: app.clone(),
clients: Default::default(),
scope: scope::Scope::new(&app.config().tauri.allowlist.http.scope),
scope: scope::Scope::new(
api.config()
.as_ref()
.map(|c| &c.scope)
.unwrap_or(&default_scope),
),
});
Ok(())
})

@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::config::HttpAllowlistScope;
use glob::Pattern;
use reqwest::Url;
use tauri::utils::config::HttpAllowlistScope;
/// Scope for filesystem access.
#[derive(Debug, Clone)]
@ -38,7 +38,7 @@ impl Scope {
#[cfg(test)]
mod tests {
use tauri::utils::config::HttpAllowlistScope;
use crate::config::HttpAllowlistScope;
#[test]
fn is_allowed() {

@ -17,6 +17,4 @@ log.workspace = true
thiserror.workspace = true
aho-corasick = "1.0"
bincode = "1"
[features]
protocol-asset = [ "tauri/protocol-asset" ]
tauri-plugin-fs = { path = "../fs", version = "0.0.0" }

@ -6,8 +6,9 @@ use aho_corasick::AhoCorasick;
use serde::{Deserialize, Serialize};
use tauri::{
plugin::{Builder, TauriPlugin},
AppHandle, FsScopeEvent, Manager, Runtime,
AppHandle, Manager, Runtime,
};
use tauri_plugin_fs::{FsExt, ScopeEvent as FsScopeEvent};
use std::{
fs::{create_dir_all, File},
@ -59,45 +60,45 @@ fn fix_pattern(ac: &AhoCorasick, s: &str) -> String {
}
fn save_scopes<R: Runtime>(app: &AppHandle<R>, app_dir: &Path, scope_state_path: &Path) {
let fs_scope = app.fs_scope();
let scope = Scope {
allowed_paths: fs_scope
.allowed_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
forbidden_patterns: fs_scope
.forbidden_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
};
let _ = create_dir_all(app_dir)
.and_then(|_| File::create(scope_state_path))
.map_err(Error::Io)
.and_then(|mut f| {
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
.map_err(Into::into)
});
if let Some(fs_scope) = app.try_fs_scope() {
let scope = Scope {
allowed_paths: fs_scope
.allowed_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
forbidden_patterns: fs_scope
.forbidden_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
};
let _ = create_dir_all(app_dir)
.and_then(|_| File::create(scope_state_path))
.map_err(Error::Io)
.and_then(|mut f| {
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
.map_err(Into::into)
});
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("persisted-scope")
.setup(|app, _api| {
let fs_scope = app.fs_scope();
#[cfg(feature = "protocol-asset")]
let asset_protocol_scope = app.asset_protocol_scope();
let fs_scope = app.try_fs_scope();
let core_scopes = app.state::<tauri::scope::Scopes>();
let app = app.clone();
let app_dir = app.path().app_data_dir();
if let Ok(app_dir) = app_dir {
let scope_state_path = app_dir.join(SCOPE_STATE_FILENAME);
let _ = fs_scope.forbid_file(&scope_state_path);
#[cfg(feature = "protocol-asset")]
let _ = asset_protocol_scope.forbid_file(&scope_state_path);
if let Some(s) = fs_scope {
let _ = s.forbid_file(&scope_state_path);
}
let _ = core_scopes.forbid_file(&scope_state_path);
// We're trying to fix broken .persisted-scope files seamlessly, so we'll be running this on the values read on the saved file.
// We will still save some semi-broken values because the scope events are quite spammy and we don't want to reduce runtime performance any further.
@ -111,16 +112,18 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
for allowed in &scope.allowed_paths {
let allowed = fix_pattern(&ac, allowed);
let _ = fs_scope.allow_file(&allowed);
#[cfg(feature = "protocol-asset")]
let _ = asset_protocol_scope.allow_file(&allowed);
if let Some(s) = fs_scope {
let _ = s.allow_file(&allowed);
}
let _ = core_scopes.allow_file(&allowed);
}
for forbidden in &scope.forbidden_patterns {
let forbidden = fix_pattern(&ac, forbidden);
let _ = fs_scope.forbid_file(&forbidden);
#[cfg(feature = "protocol-asset")]
let _ = asset_protocol_scope.forbid_file(&forbidden);
if let Some(s) = fs_scope {
let _ = s.forbid_file(&forbidden);
}
let _ = core_scopes.forbid_file(&forbidden);
}
// Manually save the fixed scopes to disk once.
@ -128,11 +131,13 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
save_scopes(&app, &app_dir, &scope_state_path);
}
fs_scope.listen(move |event| {
if let FsScopeEvent::PathAllowed(_) = event {
save_scopes(&app, &app_dir, &scope_state_path);
}
});
if let Some(s) = fs_scope {
s.listen(move |event| {
if let FsScopeEvent::PathAllowed(_) = event {
save_scopes(&app, &app_dir, &scope_state_path);
}
});
}
}
Ok(())
})

@ -0,0 +1,148 @@
use std::path::PathBuf;
use serde::{de::Error as DeError, Deserialize, Deserializer};
/// Allowlist for the shell APIs.
///
/// See more: https://tauri.app/v1/api/config#shellallowlistconfig
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Config {
/// Access scope for the binary execution APIs.
/// Sidecars are automatically enabled.
#[serde(default)]
pub scope: ShellAllowlistScope,
/// Open URL with the user's default application.
#[serde(default)]
pub open: ShellAllowlistOpen,
}
/// A command allowed to be executed by the webview API.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ShellAllowedCommand {
/// The name for this allowed shell command configuration.
///
/// This name will be used inside of the webview API to call this command along with
/// any specified arguments.
pub name: String,
/// The command name.
/// It can start with a variable that resolves to a system base directory.
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
// use default just so the schema doesn't flag it as required
pub command: PathBuf,
/// The allowed arguments for the command execution.
pub args: ShellAllowedArgs,
/// If this command is a sidecar command.
pub sidecar: bool,
}
impl<'de> Deserialize<'de> for ShellAllowedCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct InnerShellAllowedCommand {
name: String,
#[serde(rename = "cmd")]
command: Option<PathBuf>,
#[serde(default)]
args: ShellAllowedArgs,
#[serde(default)]
sidecar: bool,
}
let config = InnerShellAllowedCommand::deserialize(deserializer)?;
if !config.sidecar && config.command.is_none() {
return Err(DeError::custom(
"The shell scope `command` value is required.",
));
}
Ok(ShellAllowedCommand {
name: config.name,
command: config.command.unwrap_or_default(),
args: config.args,
sidecar: config.sidecar,
})
}
}
/// A set of command arguments allowed to be executed by the webview API.
///
/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all
/// arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to
/// be passed to the attached command configuration.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
pub enum ShellAllowedArgs {
/// Use a simple boolean to allow all or disable all arguments to this command configuration.
Flag(bool),
/// A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.
List(Vec<ShellAllowedArg>),
}
impl Default for ShellAllowedArgs {
fn default() -> Self {
Self::Flag(false)
}
}
/// A command argument allowed to be executed by the webview API.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
pub enum ShellAllowedArg {
/// A non-configurable argument that is passed to the command in the order it was specified.
Fixed(String),
/// A variable that is set while calling the command from the webview API.
///
Var {
/// [regex] validator to require passed values to conform to an expected input.
///
/// This will require the argument value passed to this variable to match the `validator` regex
/// before it will be executed.
///
/// [regex]: https://docs.rs/regex/latest/regex/#syntax
validator: String,
},
}
/// Shell scope definition.
/// It is a list of command names and associated CLI arguments that restrict the API access from the webview.
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
pub struct ShellAllowlistScope(pub Vec<ShellAllowedCommand>);
/// Defines the `shell > open` api scope.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
pub enum ShellAllowlistOpen {
/// If the shell open API should be enabled.
///
/// If enabled, the default validation regex (`^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`) is used.
Flag(bool),
/// Enable the shell open API, with a custom regex that the opened path must match against.
///
/// If using a custom regex to support a non-http(s) schema, care should be used to prevent values
/// that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`.
Validate(String),
}
impl Default for ShellAllowlistOpen {
fn default() -> Self {
Self::Flag(false)
}
}

@ -8,16 +8,17 @@ use regex::Regex;
use scope::{Scope, ScopeAllowedCommand, ScopeConfig};
use tauri::{
plugin::{Builder, TauriPlugin},
utils::config::{ShellAllowedArg, ShellAllowedArgs, ShellAllowlistOpen, ShellAllowlistScope},
AppHandle, Manager, RunEvent, Runtime,
};
mod commands;
mod config;
mod error;
mod open;
pub mod process;
mod scope;
use config::{Config, ShellAllowedArg, ShellAllowedArgs, ShellAllowlistOpen, ShellAllowlistScope};
pub use error::Error;
type Result<T> = std::result::Result<T, Error>;
type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
@ -61,25 +62,21 @@ impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("shell")
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
Builder::<R, Option<Config>>::new("shell")
.invoke_handler(tauri::generate_handler![
commands::execute,
commands::stdin_write,
commands::kill,
commands::open
])
.setup(|app, _api| {
.setup(|app, api| {
let default_config = Config::default();
let config = api.config().as_ref().unwrap_or(&default_config);
app.manage(Shell {
app: app.clone(),
children: Default::default(),
scope: Scope::new(
app,
shell_scope(
app.config().tauri.allowlist.shell.scope.clone(),
&app.config().tauri.allowlist.shell.open,
),
),
scope: Scope::new(app, shell_scope(config.scope.clone(), &config.open)),
});
Ok(())
})
@ -111,7 +108,6 @@ fn shell_scope(scope: ShellAllowlistScope, open: &ShellAllowlistOpen) -> ScopeCo
Regex::new(validator).unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
Some(validator)
}
_ => panic!("unknown shell open format, unable to prepare"),
};
ScopeConfig {
@ -136,11 +132,9 @@ fn get_allowed_clis(scope: ShellAllowlistScope) -> HashMap<String, ScopeAllowedC
.unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
scope::ScopeAllowedArg::Var { validator }
}
_ => panic!("unknown shell scope arg, unable to prepare"),
});
Some(list.collect())
}
_ => panic!("unknown shell scope command, unable to prepare"),
};
(

File diff suppressed because it is too large Load Diff

@ -40,12 +40,6 @@
"timestampUrl": ""
}
},
"updater": {
"active": false
},
"allowlist": {
"all": true
},
"windows": [
{
"title": "app",

File diff suppressed because it is too large Load Diff

@ -6,7 +6,7 @@ authors.workspace = true
license.workspace = true
[dependencies]
tauri = { workspace = true, features = ["updater", "fs-extract-api"] }
tauri.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
@ -23,6 +23,10 @@ percent-encoding = "2"
semver = { version = "1", features = [ "serde" ] }
futures-util = "0.3"
tempfile = "3"
flate2 = "1"
tar = "0.4"
ignore = "0.4"
zip = { version = "0.6", default-features = false }
[dev-dependencies]
mockito = "0.31"

@ -0,0 +1,42 @@
use serde::{Deserialize, Deserializer};
use url::Url;
/// Updater configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
#[serde(default)]
pub endpoints: Vec<UpdaterEndpoint>,
/// Additional arguments given to the NSIS or WiX installer.
#[serde(default, alias = "installer-args")]
pub installer_args: Vec<String>,
}
/// A URL to an updater server.
///
/// The URL must use the `https` scheme on production.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct UpdaterEndpoint(pub Url);
impl std::fmt::Display for UpdaterEndpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'de> Deserialize<'de> for UpdaterEndpoint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let url = Url::deserialize(deserializer)?;
#[cfg(all(not(debug_assertions), not(feature = "schema")))]
{
if url.scheme() != "https" {
return Err(serde::de::Error::custom(
"The configured updater endpoint must use the `https` protocol.",
));
}
}
Ok(Self(url))
}
}

@ -78,6 +78,12 @@ pub enum Error {
#[cfg(target_os = "linux")]
#[error("temp directory is not on the same mount point as the AppImage")]
TempDirNotOnSameMountPoint,
/// The path StripPrefixError error.
#[error("Path Error: {0}")]
PathPrefix(#[from] std::path::StripPrefixError),
/// Ignore error.
#[error("failed to walkdir: {0}")]
Ignore(#[from] ignore::Error),
}
impl Serialize for Error {

@ -6,15 +6,18 @@ use tauri::{
use tokio::sync::Mutex;
mod commands;
mod config;
mod error;
mod updater;
pub use config::Config;
pub use error::Error;
pub use updater::*;
pub type Result<T> = std::result::Result<T, Error>;
struct UpdaterState {
target: Option<String>,
config: Config,
}
struct PendingUpdate<R: Runtime>(Mutex<Option<UpdateResponse<R>>>);
@ -22,6 +25,7 @@ struct PendingUpdate<R: Runtime>(Mutex<Option<UpdateResponse<R>>>);
#[derive(Default)]
pub struct Builder {
target: Option<String>,
installer_args: Option<Vec<String>>,
}
/// Extension trait to use the updater on [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`].
@ -60,11 +64,26 @@ impl Builder {
self
}
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
pub fn installer_args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.installer_args
.replace(args.into_iter().map(Into::into).collect());
self
}
pub fn build<R: Runtime>(self) -> TauriPlugin<R, Config> {
let target = self.target;
PluginBuilder::<R>::new("updater")
.setup(move |app, _api| {
app.manage(UpdaterState { target });
let installer_args = self.installer_args;
PluginBuilder::<R, Config>::new("updater")
.setup(move |app, api| {
let mut config = api.config().clone();
if let Some(installer_args) = installer_args {
config.installer_args = installer_args;
}
app.manage(UpdaterState { target, config });
app.manage(PendingUpdate::<R>(Default::default()));
Ok(())
})

@ -2,6 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(desktop)]
use super::{
extract::{ArchiveFormat, Extract},
move_file::Move,
};
use crate::{Error, Result};
use base64::Engine;
use futures_util::StreamExt;
@ -13,8 +18,6 @@ use minisign_verify::{PublicKey, Signature};
use reqwest::ClientBuilder;
use semver::Version;
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
#[cfg(desktop)]
use tauri::api::file::{ArchiveFormat, Extract, Move};
use tauri::utils::{platform::current_exe, Env};
use tauri::{AppHandle, Manager, Runtime};
use time::OffsetDateTime;
@ -36,7 +39,7 @@ use std::{
use std::ffi::OsStr;
#[cfg(all(desktop, not(target_os = "windows")))]
use tauri::api::file::Compression;
use super::extract::Compression;
#[cfg(target_os = "windows")]
use std::{
@ -607,6 +610,7 @@ impl<R: Runtime> Update<R> {
&self.extract_path,
self.with_elevated_task,
&self.app.config(),
&self.app.state::<UpdaterState>().config,
)?;
#[cfg(not(target_os = "windows"))]
copy_files_and_run(archive_buffer, &self.extract_path)?;
@ -668,7 +672,7 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
// if something went wrong during the extraction, we should restore previous app
if let Err(err) = entry.extract(extract_path) {
Move::from_source(tmp_app_image).to_dest(extract_path)?;
return Err(tauri::api::Error::Extract(err.to_string()));
return Err(err);
}
// early finish we have everything we need here
return Ok(true);
@ -706,6 +710,7 @@ fn copy_files_and_run<R: Read + Seek>(
_extract_path: &Path,
with_elevated_task: bool,
config: &tauri::Config,
updater_config: &crate::Config,
) -> Result<()> {
// FIXME: We need to create a memory buffer with the MSI and then run it.
// (instead of extracting the MSI to a temp path)
@ -733,11 +738,11 @@ fn copy_files_and_run<R: Read + Seek>(
// Run the EXE
let mut installer = Command::new(found_path);
if tauri::utils::config::WindowsUpdateInstallMode::Quiet
== config.tauri.updater.windows.install_mode
== config.tauri.bundle.updater.install_mode
{
installer.arg("/S");
}
installer.args(&config.tauri.updater.windows.installer_args);
installer.args(&updater_config.installer_args);
installer.spawn().expect("installer failed to start");
@ -793,17 +798,17 @@ fn copy_files_and_run<R: Read + Seek>(
msi_path_arg.push(&found_path);
msi_path_arg.push("\"\"\"");
let mut msiexec_args = config
let mut msiexec_args = updater_config
.tauri
.bundle
.updater
.windows
.install_mode
.clone()
.msiexec_args()
.iter()
.map(|p| p.to_string())
.collect::<Vec<String>>();
msiexec_args.extend(config.tauri.updater.windows.installer_args.clone());
msiexec_args.extend(updater_config.installer_args.clone());
// run the installer and relaunch the application
let system_root = std::env::var("SYSTEMROOT");
@ -890,7 +895,7 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
}
}
Move::from_source(tmp_dir.path()).to_dest(extract_path)?;
return Err(tauri::api::Error::Extract(err.to_string()));
return Err(err);
}
extracted_files.push(extraction_path);

@ -0,0 +1,336 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
borrow::Cow,
fs,
io::{self, Cursor, Read, Seek},
path::{self, Path, PathBuf},
};
use crate::{Error, Result};
/// The archive reader.
#[derive(Debug)]
pub enum ArchiveReader<R: Read + Seek> {
/// A plain reader.
Plain(R),
/// A GZ- compressed reader (decoder).
GzCompressed(Box<flate2::read::GzDecoder<R>>),
}
impl<R: Read + Seek> Read for ArchiveReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::Plain(r) => r.read(buf),
Self::GzCompressed(decoder) => decoder.read(buf),
}
}
}
impl<R: Read + Seek> ArchiveReader<R> {
#[allow(dead_code)]
fn get_mut(&mut self) -> &mut R {
match self {
Self::Plain(r) => r,
Self::GzCompressed(decoder) => decoder.get_mut(),
}
}
}
/// The supported archive formats.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ArchiveFormat {
/// Tar archive.
Tar(Option<Compression>),
/// Zip archive.
#[allow(dead_code)]
Zip,
}
/// The supported compression types.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Compression {
/// Gz compression (e.g. `.tar.gz` archives)
Gz,
}
/// The zip entry.
pub struct ZipEntry {
path: PathBuf,
is_dir: bool,
file_contents: Vec<u8>,
}
/// A read-only view into an entry of an archive.
#[non_exhaustive]
pub enum Entry<'a, R: Read> {
/// An entry of a tar archive.
#[non_exhaustive]
Tar(Box<tar::Entry<'a, R>>),
/// An entry of a zip archive.
#[non_exhaustive]
#[allow(dead_code)]
Zip(ZipEntry),
}
impl<'a, R: Read> Entry<'a, R> {
/// The entry path.
pub fn path(&self) -> Result<Cow<'_, Path>> {
match self {
Self::Tar(e) => e.path().map_err(Into::into),
Self::Zip(e) => Ok(Cow::Borrowed(&e.path)),
}
}
/// Extract this entry into `into_path`.
/// If it's a directory, the target will be created, if it's a file, it'll be extracted at this location.
/// Note: You need to include the complete path, with file name and extension.
pub fn extract(self, into_path: &path::Path) -> Result<()> {
match self {
Self::Tar(mut entry) => {
// determine if it's a file or a directory
if entry.header().entry_type() == tar::EntryType::Directory {
// this is a directory, lets create it
match fs::create_dir_all(into_path) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
} else {
let mut out_file = fs::File::create(into_path)?;
io::copy(&mut entry, &mut out_file)?;
// make sure we set permissions
if let Ok(mode) = entry.header().mode() {
set_perms(into_path, Some(&mut out_file), mode, true)?;
}
}
}
Self::Zip(entry) => {
if entry.is_dir {
// this is a directory, lets create it
match fs::create_dir_all(into_path) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
} else {
let mut out_file = fs::File::create(into_path)?;
io::copy(&mut Cursor::new(entry.file_contents), &mut out_file)?;
}
}
}
Ok(())
}
}
/// The extract manager to retrieve files from archives.
pub struct Extract<'a, R: Read + Seek> {
reader: ArchiveReader<R>,
archive_format: ArchiveFormat,
tar_archive: Option<tar::Archive<&'a mut ArchiveReader<R>>>,
}
impl<'a, R: std::fmt::Debug + Read + Seek> std::fmt::Debug for Extract<'a, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Extract")
.field("reader", &self.reader)
.field("archive_format", &self.archive_format)
.finish()
}
}
impl<'a, R: Read + Seek> Extract<'a, R> {
/// Create archive from reader.
pub fn from_cursor(mut reader: R, archive_format: ArchiveFormat) -> Extract<'a, R> {
if reader.rewind().is_err() {
#[cfg(debug_assertions)]
eprintln!("Could not seek to start of the file");
}
let compression = if let ArchiveFormat::Tar(compression) = archive_format {
compression
} else {
None
};
Extract {
reader: match compression {
Some(Compression::Gz) => {
ArchiveReader::GzCompressed(Box::new(flate2::read::GzDecoder::new(reader)))
}
_ => ArchiveReader::Plain(reader),
},
archive_format,
tar_archive: None,
}
}
/// Reads the archive content.
pub fn with_files<
E: Into<Error>,
F: FnMut(Entry<'_, &mut ArchiveReader<R>>) -> std::result::Result<bool, E>,
>(
&'a mut self,
mut f: F,
) -> Result<()> {
match self.archive_format {
ArchiveFormat::Tar(_) => {
let archive = tar::Archive::new(&mut self.reader);
self.tar_archive.replace(archive);
for entry in self.tar_archive.as_mut().unwrap().entries()? {
let entry = entry?;
if entry.path().is_ok() {
let stop = f(Entry::Tar(Box::new(entry))).map_err(Into::into)?;
if stop {
break;
}
}
}
}
ArchiveFormat::Zip => {
#[cfg(feature = "fs-extract-api")]
{
let mut archive = zip::ZipArchive::new(self.reader.get_mut())?;
let file_names = archive
.file_names()
.map(|f| f.to_string())
.collect::<Vec<String>>();
for path in file_names {
let mut zip_file = archive.by_name(&path)?;
let is_dir = zip_file.is_dir();
let mut file_contents = Vec::new();
zip_file.read_to_end(&mut file_contents)?;
let stop = f(Entry::Zip(ZipEntry {
path: path.into(),
is_dir,
file_contents,
}))
.map_err(Into::into)?;
if stop {
break;
}
}
}
}
}
Ok(())
}
/// Extract an entire source archive into a specified path. If the source is a single compressed
/// file and not an archive, it will be extracted into a file with the same name inside of
/// `into_dir`.
#[allow(dead_code)]
pub fn extract_into(&mut self, into_dir: &path::Path) -> Result<()> {
match self.archive_format {
ArchiveFormat::Tar(_) => {
let mut archive = tar::Archive::new(&mut self.reader);
archive.unpack(into_dir)?;
}
ArchiveFormat::Zip => {
#[cfg(feature = "fs-extract-api")]
{
let mut archive = zip::ZipArchive::new(self.reader.get_mut())?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
// Decode the file name from raw bytes instead of using file.name() directly.
// file.name() uses String::from_utf8_lossy() which may return messy characters
// such as: 爱交易.app/, that does not work as expected.
// Here we require the file name must be a valid UTF-8.
let file_name = String::from_utf8(file.name_raw().to_vec())?;
let out_path = into_dir.join(file_name);
if file.is_dir() {
fs::create_dir_all(&out_path)?;
} else {
if let Some(out_path_parent) = out_path.parent() {
fs::create_dir_all(out_path_parent)?;
}
let mut out_file = fs::File::create(&out_path)?;
io::copy(&mut file, &mut out_file)?;
}
// Get and Set permissions
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&out_path, fs::Permissions::from_mode(mode))?;
}
}
}
}
}
}
Ok(())
}
}
fn set_perms(
dst: &Path,
f: Option<&mut std::fs::File>,
mode: u32,
preserve: bool,
) -> io::Result<()> {
_set_perms(dst, f, mode, preserve).map_err(|_| {
io::Error::new(
io::ErrorKind::Other,
format!(
"failed to set permissions to {mode:o} \
for `{}`",
dst.display()
),
)
})
}
#[cfg(unix)]
fn _set_perms(
dst: &Path,
f: Option<&mut std::fs::File>,
mode: u32,
preserve: bool,
) -> io::Result<()> {
use std::os::unix::prelude::*;
let mode = if preserve { mode } else { mode & 0o777 };
let perm = fs::Permissions::from_mode(mode as _);
match f {
Some(f) => f.set_permissions(perm),
None => fs::set_permissions(dst, perm),
}
}
#[cfg(windows)]
fn _set_perms(
dst: &Path,
f: Option<&mut std::fs::File>,
mode: u32,
_preserve: bool,
) -> io::Result<()> {
if mode & 0o200 == 0o200 {
return Ok(());
}
match f {
Some(f) => {
let mut perm = f.metadata()?.permissions();
perm.set_readonly(true);
f.set_permissions(perm)
}
None => {
let mut perm = fs::metadata(dst)?.permissions();
perm.set_readonly(true);
fs::set_permissions(dst, perm)
}
}
}

@ -12,6 +12,8 @@
//! ```
mod core;
mod extract;
mod move_file;
use std::time::Duration;
@ -23,7 +25,7 @@ pub use self::core::{DownloadEvent, RemoteRelease};
use tauri::{AppHandle, Manager, Runtime};
use crate::Result;
use crate::{Result, UpdaterState};
/// Gets the target string used on the updater.
pub fn target() -> Option<String> {
@ -276,7 +278,7 @@ impl<R: Runtime> UpdateResponse<R> {
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
self.update
.download_and_install(
self.update.app.config().tauri.updater.pubkey.clone(),
self.update.app.config().tauri.bundle.updater.pubkey.clone(),
on_event,
)
.await
@ -285,14 +287,13 @@ impl<R: Runtime> UpdateResponse<R> {
/// Initializes the [`UpdateBuilder`] using the app configuration.
pub fn builder<R: Runtime>(handle: AppHandle<R>) -> UpdateBuilder<R> {
let updater_config = &handle.config().tauri.updater;
let package_info = handle.package_info().clone();
// prepare our endpoints
let endpoints = updater_config
let endpoints = handle
.state::<UpdaterState>()
.config
.endpoints
.as_ref()
.expect("Something wrong with endpoints")
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>();

@ -0,0 +1,118 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use ignore::WalkBuilder;
use std::{fs, path};
use crate::Result;
/// Moves a file from the given path to the specified destination.
///
/// `source` and `dest` must be on the same filesystem.
/// If `replace_using_temp` is specified, the destination file will be
/// replaced using the given temporary path.
///
/// * Errors:
/// * Io - copying / renaming
#[derive(Debug)]
pub struct Move<'a> {
source: &'a path::Path,
temp: Option<&'a path::Path>,
}
impl<'a> Move<'a> {
/// Specify source file
pub fn from_source(source: &'a path::Path) -> Move<'a> {
Self { source, temp: None }
}
/// If specified and the destination file already exists, the "destination"
/// file will be moved to the given temporary location before the "source"
/// file is moved to the "destination" file.
///
/// In the event of an `io` error while renaming "source" to "destination",
/// the temporary file will be moved back to "destination".
///
/// The `temp` dir must be explicitly provided since `rename` operations require
/// files to live on the same filesystem.
#[allow(dead_code)]
pub fn replace_using_temp(&mut self, temp: &'a path::Path) -> &mut Self {
self.temp = Some(temp);
self
}
/// Move source file to specified destination (replace whole directory)
pub fn to_dest(&self, dest: &path::Path) -> Result<()> {
match self.temp {
None => {
fs::rename(self.source, dest)?;
}
Some(temp) => {
if dest.exists() {
fs::rename(dest, temp)?;
if let Err(e) = fs::rename(self.source, dest) {
fs::rename(temp, dest)?;
return Err(e.into());
}
} else {
fs::rename(self.source, dest)?;
}
}
};
Ok(())
}
/// Walk in the source and copy all files and create directories if needed by
/// replacing existing elements. (equivalent to a cp -R)
#[allow(dead_code)]
pub fn walk_to_dest(&self, dest: &path::Path) -> Result<()> {
match self.temp {
None => {
// got no temp -- no need to backup
walkdir_and_copy(self.source, dest)?;
}
Some(temp) => {
if dest.exists() {
// we got temp and our dest exist, lets make a backup
// of current files
walkdir_and_copy(dest, temp)?;
if let Err(e) = walkdir_and_copy(self.source, dest) {
// if we got something wrong we reset the dest with our backup
fs::rename(temp, dest)?;
return Err(e);
}
} else {
// got temp but dest didnt exist
walkdir_and_copy(self.source, dest)?;
}
}
};
Ok(())
}
}
// Walk into the source and create directories, and copy files
// Overwriting existing items but keeping untouched the files in the dest
// not provided in the source.
fn walkdir_and_copy(source: &path::Path, dest: &path::Path) -> Result<()> {
let walkdir = WalkBuilder::new(source).hidden(false).build();
for entry in walkdir {
// Check if it's a file
let element = entry?;
let metadata = element.metadata()?;
let destination = dest.join(element.path().strip_prefix(source)?);
// we make sure it's a directory and destination doesnt exist
if metadata.is_dir() && !&destination.exists() {
fs::create_dir_all(&destination)?;
}
// we make sure it's a file
if metadata.is_file() {
fs::copy(element.path(), destination)?;
}
}
Ok(())
}

File diff suppressed because it is too large Load Diff

@ -7,24 +7,29 @@
use tauri_plugin_updater::UpdaterExt;
fn main() {
#[allow(unused_mut)]
let mut context = tauri::generate_context!();
let mut updater = tauri_plugin_updater::Builder::new();
if std::env::var("TARGET").unwrap_or_default() == "nsis" {
// /D sets the default installation directory ($INSTDIR),
// overriding InstallDir and InstallDirRegKey.
// It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces.
// Only absolute paths are supported.
// NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder
context.config_mut().tauri.updater.windows.installer_args = vec![format!(
// TODO mutate plugin config
updater = updater.installer_args(vec![format!(
"/D={}",
tauri::utils::platform::current_exe()
.unwrap()
.parent()
.unwrap()
.display()
)];
)]);
}
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(updater.build())
.setup(|app| {
let handle = app.handle();
tauri::async_runtime::spawn(async move {

@ -21,18 +21,19 @@
"wix": {
"skipWebviewInstall": true
}
},
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
"windows": {
"installMode": "quiet"
}
}
},
"allowlist": {
"all": false
},
}
},
"plugins": {
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
"endpoints": ["http://localhost:3007"],
"windows": {
"installMode": "quiet"
}
"endpoints": ["http://localhost:3007"]
}
}
}

File diff suppressed because it is too large Load Diff

@ -33,9 +33,6 @@
"exceptionDomain": ""
}
},
"allowlist": {
"all": false
},
"windows": [
{
"title": "Tauri App",

Loading…
Cancel
Save