diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index c5168a4db..892399a9d 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -97,3 +97,6 @@ plugin = ["mlua"] [[bin]] path = "src/main.rs" name = "dx" + +[dev-dependencies] +tempfile = "3.3" diff --git a/packages/cli/docs/src/cmd/README.md b/packages/cli/docs/src/cmd/README.md index 93f6f5573..735794635 100644 --- a/packages/cli/docs/src/cmd/README.md +++ b/packages/cli/docs/src/cmd/README.md @@ -15,6 +15,7 @@ Commands: serve Build, watch & serve the Rust WASM app and all of its assets create Init a new project for Dioxus clean Clean output artifacts + bundle Bundle the Rust desktop app and all of its assets version Print the version of this extension fmt Format some rsx check Check the Rust files in the project for issues diff --git a/packages/cli/docs/src/configure.md b/packages/cli/docs/src/configure.md index 8da66d9ec..9d0cb0a9b 100644 --- a/packages/cli/docs/src/configure.md +++ b/packages/cli/docs/src/configure.md @@ -144,7 +144,7 @@ Configeration related to static resources your application uses in development: Configeration related to any proxies your application requires durring development. Proxies will forward requests to a new service ``` -[web.proxy] +[[web.proxy]] # configuration ``` diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index b25576120..3fa8e7818 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -236,10 +236,9 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { } } - let t_end = std::time::Instant::now(); Ok(BuildResult { warnings: warning_messages, - elapsed_time: (t_end - t_start).as_millis(), + elapsed_time: t_start.elapsed().as_millis(), }) } @@ -360,7 +359,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result String { - let prefix = if !val.starts_with('/') { "/" } else { "" }; - let suffix = if !val.ends_with('/') { "/" } else { "" }; - format!("{}{}{}", prefix, val, suffix) -} - /// Config options for the bundling system. #[derive(Clone, Debug, Default, Deserialize, Parser)] pub struct ConfigOptsBundle { diff --git a/packages/cli/src/cli/check/mod.rs b/packages/cli/src/cli/check.rs similarity index 100% rename from packages/cli/src/cli/check/mod.rs rename to packages/cli/src/cli/check.rs diff --git a/packages/cli/src/cli/config.rs b/packages/cli/src/cli/config.rs index cde29ef6a..45fcc2f23 100644 --- a/packages/cli/src/cli/config.rs +++ b/packages/cli/src/cli/config.rs @@ -1,6 +1,6 @@ use super::*; -/// Build the Rust WASM app and all of its assets. +/// Dioxus config file controls #[derive(Clone, Debug, Deserialize, Subcommand)] #[clap(name = "config")] pub enum Config { diff --git a/packages/cli/src/cli/create.rs b/packages/cli/src/cli/create.rs index b2323d0a2..92b0334f5 100644 --- a/packages/cli/src/cli/create.rs +++ b/packages/cli/src/cli/create.rs @@ -46,15 +46,15 @@ impl Create { toml.as_table_mut().fmt(); let as_string = toml.to_string(); - let new_string = remove_tripple_newlines(&as_string); + let new_string = remove_triple_newlines(&as_string); let mut file = std::fs::File::create(toml_path)?; file.write_all(new_string.as_bytes())?; } - // remove any tripple newlines from the readme + // remove any triple newlines from the readme let readme_path = path.join("README.md"); let readme = std::fs::read_to_string(&readme_path)?; - let new_readme = remove_tripple_newlines(&readme); + let new_readme = remove_triple_newlines(&readme); let mut file = std::fs::File::create(readme_path)?; file.write_all(new_readme.as_bytes())?; @@ -64,7 +64,7 @@ impl Create { } } -fn remove_tripple_newlines(string: &str) -> String { +fn remove_triple_newlines(string: &str) -> String { let mut new_string = String::new(); for char in string.chars() { if char == '\n' && new_string.ends_with("\n\n") { diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index 37d64d2f3..9b3be33bc 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -72,6 +72,7 @@ pub enum Commands { #[clap(name = "fmt")] Autoformat(autoformat::Autoformat), + /// Check the Rust files in the project for issues. #[clap(name = "check")] Check(check::Check), diff --git a/packages/cli/src/cli/plugin.rs b/packages/cli/src/cli/plugin.rs index 9f514edec..43375f700 100644 --- a/packages/cli/src/cli/plugin.rs +++ b/packages/cli/src/cli/plugin.rs @@ -2,7 +2,7 @@ use super::*; -/// Build the Rust WASM app and all of its assets. +/// Manage plugins for dioxus cli #[derive(Clone, Debug, Deserialize, Subcommand)] #[clap(name = "plugin")] pub enum Plugin { diff --git a/packages/cli/src/cli/tool.rs b/packages/cli/src/cli/tool.rs deleted file mode 100644 index 2edccf7ea..000000000 --- a/packages/cli/src/cli/tool.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::tools; - -use super::*; - -/// Build the Rust WASM app and all of its assets. -#[derive(Clone, Debug, Deserialize, Subcommand)] -#[clap(name = "tool")] -pub enum Tool { - /// Return all dioxus-cli support tools. - List {}, - /// Get default app install path. - AppPath {}, - /// Install a new tool. - Add { name: String }, -} - -impl Tool { - pub async fn tool(self) -> Result<()> { - match self { - Tool::List {} => { - for item in tools::tool_list() { - if tools::Tool::from_str(item).unwrap().is_installed() { - println!("- {item} [installed]"); - } else { - println!("- {item}"); - } - } - } - Tool::AppPath {} => { - if let Some(v) = tools::tools_path().to_str() { - println!("{}", v); - } else { - return custom_error!("Tools path get failed."); - } - } - Tool::Add { name } => { - let tool_list = tools::tool_list(); - - if !tool_list.contains(&name.as_str()) { - return custom_error!("Tool {name} not found."); - } - let target_tool = tools::Tool::from_str(&name).unwrap(); - - if target_tool.is_installed() { - log::warn!("Tool {name} is installed."); - return Ok(()); - } - - log::info!("Start to download tool package..."); - if let Err(e) = target_tool.download_package().await { - return custom_error!("Tool download failed: {e}"); - } - - log::info!("Start to install tool package..."); - if let Err(e) = target_tool.install_package().await { - return custom_error!("Tool install failed: {e}"); - } - - log::info!("Tool {name} installed successfully!"); - } - } - Ok(()) - } -} diff --git a/packages/cli/src/cli/translate.rs b/packages/cli/src/cli/translate.rs index a922d428d..d3901e651 100644 --- a/packages/cli/src/cli/translate.rs +++ b/packages/cli/src/cli/translate.rs @@ -4,7 +4,7 @@ use dioxus_rsx::{BodyNode, CallBody}; use super::*; -/// Build the Rust WASM app and all of its assets. +/// Translate some source file into Dioxus code #[derive(Clone, Debug, Parser)] #[clap(name = "translate")] pub struct Translate { diff --git a/packages/cli/src/cli/version.rs b/packages/cli/src/cli/version.rs index 3c77ba419..c015a73a2 100644 --- a/packages/cli/src/cli/version.rs +++ b/packages/cli/src/cli/version.rs @@ -1,6 +1,6 @@ use super::*; -/// Build the Rust WASM app and all of its assets. +/// Print the version of this extension #[derive(Clone, Debug, Parser)] #[clap(name = "version")] pub struct Version {} diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index 7832ab326..84b9d4b71 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -35,6 +35,9 @@ pub enum Error { #[error("Invalid proxy URL: {0}")] InvalidProxy(#[from] hyper::http::uri::InvalidUri), + #[error("Failed to establish proxy: {0}")] + ProxySetupError(String), + #[error("Error proxying request: {0}")] ProxyRequestError(hyper::Error), diff --git a/packages/cli/src/server/web/proxy.rs b/packages/cli/src/server/web/proxy.rs index 8de65f4db..e35635b6b 100644 --- a/packages/cli/src/server/web/proxy.rs +++ b/packages/cli/src/server/web/proxy.rs @@ -48,6 +48,16 @@ impl ProxyClient { pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result { let url: Uri = proxy.backend.parse()?; let path = url.path().to_string(); + let trimmed_path = path.trim_end_matches('/'); + + if trimmed_path.is_empty() { + return Err(crate::Error::ProxySetupError(format!( + "Proxy backend URL must have a non-empty path, e.g. {}/api instead of {}", + proxy.backend.trim_end_matches('/'), + proxy.backend + ))); + } + let client = ProxyClient::new(url); // We also match everything after the path using a wildcard matcher. @@ -56,7 +66,7 @@ pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result { router = router.route( // Always remove trailing /'s so that the exact route // matches. - path.trim_end_matches('/'), + trimmed_path, any(move |req| async move { client .send(req) @@ -68,7 +78,7 @@ pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result { // Wildcard match anything else _after_ the backend URL's path. // Note that we know `path` ends with a trailing `/` in this branch, // so `wildcard` will look like `http://localhost/api/*proxywildcard`. - let wildcard = format!("{}/*proxywildcard", path.trim_end_matches('/')); + let wildcard = format!("{}/*proxywildcard", trimmed_path); router = router.route( &wildcard, any(move |req| async move { @@ -168,4 +178,21 @@ mod test { async fn add_proxy_trailing_slash() { test_proxy_requests("/api/".to_string()).await; } + + #[test] + fn add_proxy_empty_path() { + let config = WebProxyConfig { + backend: "http://localhost:8000".to_string(), + }; + let router = super::add_proxy(Router::new(), &config); + match router.unwrap_err() { + crate::Error::ProxySetupError(e) => { + assert_eq!( + e, + "Proxy backend URL must have a non-empty path, e.g. http://localhost:8000/api instead of http://localhost:8000" + ); + } + e => panic!("Unexpected error type: {}", e), + } + } } diff --git a/packages/cli/src/tools.rs b/packages/cli/src/tools.rs index 98d4f9dbe..4045b236e 100644 --- a/packages/cli/src/tools.rs +++ b/packages/cli/src/tools.rs @@ -328,22 +328,79 @@ pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> { } for i in 0..zip.len() { - let mut file = zip.by_index(i)?; - if file.is_dir() { - // dir - let target = target.join(Path::new(&file.name().replace('\\', ""))); - std::fs::create_dir_all(target)?; + let mut zip_entry = zip.by_index(i)?; + + // check for dangerous paths + // see https://docs.rs/zip/latest/zip/read/struct.ZipFile.html#warnings + let Some(enclosed_name) = zip_entry.enclosed_name() else { + return Err(anyhow::anyhow!( + "Refusing to unpack zip entry with potentially dangerous path: zip={} entry={:?}", + file.display(), + zip_entry.name() + )); + }; + + let output_path = target.join(enclosed_name); + if zip_entry.is_dir() { + std::fs::create_dir_all(output_path)?; } else { - // file - let file_path = target.join(Path::new(file.name())); - let mut target_file = if !file_path.exists() { - std::fs::File::create(file_path)? + // create parent dirs if needed + if let Some(parent) = output_path.parent() { + std::fs::create_dir_all(parent)?; + } + + // extract file + let mut target_file = if !output_path.exists() { + std::fs::File::create(output_path)? } else { - std::fs::File::open(file_path)? + std::fs::File::open(output_path)? }; - let _num = std::io::copy(&mut file, &mut target_file)?; + let _num = std::io::copy(&mut zip_entry, &mut target_file)?; } } Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + use std::path::PathBuf; + use tempfile::tempdir; + + #[test] + fn test_extract_zip() -> anyhow::Result<()> { + let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("tests/fixtures/test.zip"); + let temp_dir = tempdir()?; + let temp_path = temp_dir.path(); + + extract_zip(path.as_path(), temp_path)?; + + let expected_files = vec!["file1.txt", "file2.txt", "dir/file3.txt"]; + for file in expected_files { + let path = temp_path.join(file); + assert!(path.exists(), "File not found: {:?}", path); + } + + Ok(()) + } + + #[test] + fn test_extract_zip_dangerous_path() -> anyhow::Result<()> { + let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("tests/fixtures/dangerous.zip"); + let temp_dir = tempdir()?; + let temp_path = temp_dir.path(); + + let result = extract_zip(path.as_path(), temp_path); + + let err = result.unwrap_err(); + assert!(err + .to_string() + .contains("Refusing to unpack zip entry with potentially dangerous path: zip=")); + assert!(err.to_string().contains("entry=\"/etc/passwd\"")); + + Ok(()) + } +} diff --git a/packages/cli/tests/fixtures/dangerous.zip b/packages/cli/tests/fixtures/dangerous.zip new file mode 100644 index 000000000..09ff13d63 Binary files /dev/null and b/packages/cli/tests/fixtures/dangerous.zip differ diff --git a/packages/cli/tests/fixtures/test.zip b/packages/cli/tests/fixtures/test.zip new file mode 100644 index 000000000..bb90b81ad Binary files /dev/null and b/packages/cli/tests/fixtures/test.zip differ diff --git a/packages/hot-reload/src/file_watcher.rs b/packages/hot-reload/src/file_watcher.rs index 9bbfcb512..48d97dfa7 100644 --- a/packages/hot-reload/src/file_watcher.rs +++ b/packages/hot-reload/src/file_watcher.rs @@ -248,7 +248,7 @@ pub fn init(cfg: Config) { return shutdown; } else if log { println!( - "Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes." + "Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes." ); } true diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index 293b51b9e..bc561bf1b 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -1,4 +1,4 @@ -//! Handles quering data from the renderer +//! Handles querying data from the renderer use euclid::Rect; diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index baa3c6fe5..8de282869 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-html = { workspace = true, features = ["wasm-bind"], default-features = false } +dioxus-html = { workspace = true, features = ["wasm-bind"] } dioxus-interpreter-js = { workspace = true, features = [ "sledgehammer", "minimal_bindings",