diff --git a/crates/desktop/artcraft/src/core/utils/download_url_to_user_download_dir.rs b/crates/desktop/artcraft/src/core/utils/download_url_to_user_download_dir.rs index c05205699d..02a3d414bc 100644 --- a/crates/desktop/artcraft/src/core/utils/download_url_to_user_download_dir.rs +++ b/crates/desktop/artcraft/src/core/utils/download_url_to_user_download_dir.rs @@ -15,46 +15,47 @@ pub async fn download_url_to_user_download_dir( app_prefs: &AppPreferences, ) -> Result { - let url_path = match url.to_file_path() { - Ok(path) => path, - Err(err) => { - error!("Could not get file path from URL: {:?}", err); - return Err(ArtcraftError::AnyhowError(anyhow!("Could not get file path from URL: {:?}", err))); - } - }; + let path_segments = url.path_segments() + .ok_or_else(|| ArtcraftError::AnyhowError(anyhow!("URL does not have path segments (1): {:?}", url)))?; - let url_file_name = url_path - .file_name() - .ok_or(ArtcraftError::AnyhowError(anyhow!( - "No file name for path: {:?} at url: {:?}", &url_path, &url)))? - .to_str() - .ok_or(ArtcraftError::AnyhowError(anyhow!( - "No file name for path: {:?} at url: {:?}", &url_path, &url)))? + let mut url_file_name = path_segments.last() + .ok_or_else(|| ArtcraftError::AnyhowError(anyhow!("URL does not have path segments (2): {:?}", url)))? + .trim() .to_string(); + if !url_file_name.starts_with("artcraft") { + url_file_name = format!("artcraft_{}", url_file_name); + } + + let url_file_name = url_file_name; + check_url_file_name_for_downloadability(&url_file_name)?; let download_directory = app_prefs .preferred_download_directory .download_directory(app_data_root); + info!("Pushing path `{}` onto downloaded directory `{:?}`", url_file_name, download_directory); + let download_filename = { let mut download_filename = download_directory.clone(); download_filename.push(&url_file_name); - download_directory + download_filename }; + info!("Download filename: `{:?}`", download_filename); + if download_filename.exists() { return Err(ArtcraftError::CannotDownloadFilePathAlreadyExists { path: download_filename }.into()); } - + info!("Downloading bytes from url: {:?}", url); let response = reqwest::get(url.clone()).await?; let response_bytes = response.bytes().await?; info!("Writing file: {:?}", download_filename); - + let mut file = OpenOptions::new() .create(true) .write(true) @@ -68,6 +69,16 @@ pub async fn download_url_to_user_download_dir( // TODO: This likely needs more protections. fn check_url_file_name_for_downloadability(filename: &str) -> Result<(), ArtcraftError> { + if filename.trim().is_empty() { + error!("Download filename is empty!"); + return Err(ArtcraftError::BadDownloadFilename { path: "".into() }.into()); + } + + if !filename.is_ascii() { + error!("Download filename is not completely ASCII: {:?}", filename); + return Err(ArtcraftError::BadDownloadFilename { path: filename.into() }.into()); + } + if filename.contains("/") || filename.contains("..") || filename.contains("\\") @@ -76,6 +87,17 @@ fn check_url_file_name_for_downloadability(filename: &str) -> Result<(), Artcraf return Err(ArtcraftError::BadDownloadFilename { path: filename.into() }.into()); } + if filename.contains("'") + || filename.contains("\"") + || filename.contains("%") + || filename.contains("<") + || filename.contains(">") + || filename.contains("|") + { + error!("Download filename has dangerous path components: {:?}", filename); + return Err(ArtcraftError::BadDownloadFilename { path: filename.into() }.into()); + } + if filename.ends_with("apk") || filename.ends_with("app") || filename.ends_with("bat")