aboutsummaryrefslogtreecommitdiffstats
path: root/src/file_upload.rs
diff options
context:
space:
mode:
authorSven-Hendrik Haase <svenstaro@gmail.com>2023-09-24 11:21:29 +0000
committerGitHub <noreply@github.com>2023-09-24 11:21:29 +0000
commitfa15976c1b4b070ad1bb8cecff23b7d571959852 (patch)
tree8a6e756e84f4d301d0829db5cd3976d888030215 /src/file_upload.rs
parentMerge pull request #1237 from svenstaro/fix-ci (diff)
parentFix clippy complaints (diff)
downloadminiserve-fa15976c1b4b070ad1bb8cecff23b7d571959852.tar.gz
miniserve-fa15976c1b4b070ad1bb8cecff23b7d571959852.zip
Merge pull request #1228 from cyqsimon/upload-refactor
Minor refactor on upload code
Diffstat (limited to 'src/file_upload.rs')
-rw-r--r--src/file_upload.rs299
1 files changed, 0 insertions, 299 deletions
diff --git a/src/file_upload.rs b/src/file_upload.rs
deleted file mode 100644
index 2275c73..0000000
--- a/src/file_upload.rs
+++ /dev/null
@@ -1,299 +0,0 @@
-use std::{
- io::Write,
- path::{Component, Path, PathBuf},
-};
-
-use actix_web::{http::header, HttpRequest, HttpResponse};
-use futures::TryStreamExt;
-
-use crate::errors::ContextualError;
-use crate::listing;
-
-/// Saves file data from a multipart form field (`field`) to `file_path`, optionally overwriting
-/// existing file.
-///
-/// Returns total bytes written to file.
-async fn save_file(
- field: actix_multipart::Field,
- file_path: PathBuf,
- overwrite_files: bool,
-) -> Result<u64, ContextualError> {
- if !overwrite_files && file_path.exists() {
- return Err(ContextualError::DuplicateFileError);
- }
-
- let file = std::fs::File::create(&file_path).map_err(|e| {
- ContextualError::IoError(format!("Failed to create {}", file_path.display()), e)
- })?;
-
- let (_, written_len) = field
- .map_err(ContextualError::MultipartError)
- .try_fold((file, 0u64), |(mut file, written_len), bytes| async move {
- file.write_all(bytes.as_ref())
- .map_err(|e| ContextualError::IoError("Failed to write to file".to_string(), e))?;
- Ok((file, written_len + bytes.len() as u64))
- })
- .await?;
-
- Ok(written_len)
-}
-
-/// Handles a single field in a multipart form
-async fn handle_multipart(
- mut field: actix_multipart::Field,
- path: PathBuf,
- overwrite_files: bool,
- allow_mkdir: bool,
- allow_hidden_paths: bool,
- allow_symlinks: bool,
-) -> Result<u64, ContextualError> {
- let field_name = field.name().to_string();
-
- match std::fs::metadata(&path) {
- Err(_) => Err(ContextualError::InsufficientPermissionsError(
- path.display().to_string(),
- )),
- Ok(metadata) if !metadata.is_dir() => Err(ContextualError::InvalidPathError(format!(
- "cannot upload file to {}, since it's not a directory",
- &path.display()
- ))),
- Ok(metadata) if metadata.permissions().readonly() => Err(
- ContextualError::InsufficientPermissionsError(path.display().to_string()),
- ),
- Ok(_) => Ok(()),
- }?;
-
- if field_name == "mkdir" {
- if !allow_mkdir {
- return Err(ContextualError::InsufficientPermissionsError(
- path.display().to_string(),
- ));
- }
-
- let mut user_given_path = PathBuf::new();
- let mut absolute_path = path.clone();
-
- // Get the path the user gave
- let mkdir_path_bytes = field.try_next().await;
- match mkdir_path_bytes {
- Ok(Some(mkdir_path_bytes)) => {
- let mkdir_path = std::str::from_utf8(&mkdir_path_bytes).map_err(|e| {
- ContextualError::ParseError(
- "Failed to parse 'mkdir' path".to_string(),
- e.to_string(),
- )
- })?;
- let mkdir_path = mkdir_path.replace('\\', "/");
- absolute_path.push(&mkdir_path);
- user_given_path.push(&mkdir_path);
- }
- _ => {
- return Err(ContextualError::ParseError(
- "Failed to parse 'mkdir' path".to_string(),
- "".to_string(),
- ))
- }
- };
-
- // Disallow using `..` (parent) in mkdir path
- if user_given_path
- .components()
- .any(|c| c == Component::ParentDir)
- {
- return Err(ContextualError::InvalidPathError(
- "Cannot use '..' in mkdir path".to_string(),
- ));
- }
- // Hidden paths check
- sanitize_path(&user_given_path, allow_hidden_paths).ok_or_else(|| {
- ContextualError::InvalidPathError("Cannot use hidden paths in mkdir path".to_string())
- })?;
-
- // Ensure there are no illegal symlinks
- if !allow_symlinks && contains_symlink(&absolute_path) {
- return Err(ContextualError::InsufficientPermissionsError(
- user_given_path.display().to_string(),
- ));
- }
-
- std::fs::create_dir_all(&absolute_path).map_err(|e| {
- ContextualError::IoError(format!("Failed to create {}", user_given_path.display()), e)
- })?;
-
- return Ok(0);
- }
-
- let filename = field.content_disposition().get_filename().ok_or_else(|| {
- ContextualError::ParseError(
- "HTTP header".to_string(),
- "Failed to retrieve the name of the file to upload".to_string(),
- )
- })?;
-
- let filename_path = sanitize_path(Path::new(&filename), false).ok_or_else(|| {
- ContextualError::InvalidPathError("Invalid file name to upload".to_string())
- })?;
-
- // Ensure there are no illegal symlinks in the file upload path
- if !allow_symlinks && contains_symlink(&path) {
- return Err(ContextualError::InsufficientPermissionsError(
- filename.to_string(),
- ));
- }
-
- save_file(field, path.join(filename_path), overwrite_files).await
-}
-
-/// Handle incoming request to upload a file or create a directory.
-/// Target file path is expected as path parameter in URI and is interpreted as relative from
-/// server root directory. Any path which will go outside of this directory is considered
-/// invalid.
-/// This method returns future.
-pub async fn upload_file(
- req: HttpRequest,
- payload: actix_web::web::Payload,
-) -> Result<HttpResponse, ContextualError> {
- let conf = req.app_data::<crate::MiniserveConfig>().unwrap();
- let return_path = if let Some(header) = req.headers().get(header::REFERER) {
- header.to_str().unwrap_or("/").to_owned()
- } else {
- "/".to_string()
- };
-
- let query_params = listing::extract_query_parameters(&req);
- let upload_path = query_params.path.as_ref().ok_or_else(|| {
- ContextualError::InvalidHttpRequestError("Missing query parameter 'path'".to_string())
- })?;
- let upload_path = sanitize_path(upload_path, conf.show_hidden).ok_or_else(|| {
- ContextualError::InvalidPathError("Invalid value for 'path' parameter".to_string())
- })?;
-
- let app_root_dir = conf.path.canonicalize().map_err(|e| {
- ContextualError::IoError("Failed to resolve path served by miniserve".to_string(), e)
- })?;
-
- // Disallow paths outside of allowed directories
- let upload_allowed = conf.allowed_upload_dir.is_empty()
- || conf
- .allowed_upload_dir
- .iter()
- .any(|s| upload_path.starts_with(s));
-
- if !upload_allowed {
- return Err(ContextualError::UploadForbiddenError);
- }
-
- // Disallow the target path to go outside of the served directory
- // The target directory shouldn't be canonicalized when it gets passed to
- // handle_multipart so that it can check for symlinks if needed
- let non_canonicalized_target_dir = app_root_dir.join(upload_path);
- match non_canonicalized_target_dir.canonicalize() {
- Ok(path) if !conf.no_symlinks => Ok(path),
- Ok(path) if path.starts_with(&app_root_dir) => Ok(path),
- _ => Err(ContextualError::InvalidHttpRequestError(
- "Invalid value for 'path' parameter".to_string(),
- )),
- }?;
-
- actix_multipart::Multipart::new(req.headers(), payload)
- .map_err(ContextualError::MultipartError)
- .and_then(|field| {
- handle_multipart(
- field,
- non_canonicalized_target_dir.clone(),
- conf.overwrite_files,
- conf.mkdir_enabled,
- conf.show_hidden,
- !conf.no_symlinks,
- )
- })
- .try_collect::<Vec<u64>>()
- .await?;
-
- Ok(HttpResponse::SeeOther()
- .append_header((header::LOCATION, return_path))
- .finish())
-}
-
-/// Guarantee that the path is relative and cannot traverse back to parent directories
-/// and optionally prevent traversing hidden directories.
-///
-/// See the unit tests tests::test_sanitize_path* for examples
-pub fn sanitize_path(path: &Path, traverse_hidden: bool) -> Option<PathBuf> {
- let mut buf = PathBuf::new();
-
- for comp in path.components() {
- match comp {
- Component::Normal(name) => buf.push(name),
- Component::ParentDir => {
- buf.pop();
- }
- _ => (),
- }
- }
-
- // Double-check that all components are Normal and check for hidden dirs
- for comp in buf.components() {
- match comp {
- Component::Normal(_) if traverse_hidden => (),
- Component::Normal(name) if !name.to_str()?.starts_with('.') => (),
- _ => return None,
- }
- }
-
- Some(buf)
-}
-
-/// Returns if a path goes through a symolic link
-fn contains_symlink(path: &PathBuf) -> bool {
- let mut joined_path = PathBuf::new();
- for path_slice in path {
- joined_path = joined_path.join(path_slice);
- if !joined_path.exists() {
- // On Windows, `\\?\` won't exist even though it's the root
- // So, we can't just return here
- // But we don't need to check if it's a symlink since it won't be
- continue;
- }
- if joined_path
- .symlink_metadata()
- .map(|m| m.file_type().is_symlink())
- .unwrap_or(false)
- {
- return true;
- }
- }
- false
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use pretty_assertions::assert_eq;
- use rstest::rstest;
-
- #[rstest]
- #[case("/foo", "foo")]
- #[case("////foo", "foo")]
- #[case("C:/foo", if cfg!(windows) { "foo" } else { "C:/foo" })]
- #[case("../foo", "foo")]
- #[case("../foo/../bar/abc", "bar/abc")]
- fn test_sanitize_path(#[case] input: &str, #[case] output: &str) {
- assert_eq!(
- sanitize_path(Path::new(input), true).unwrap(),
- Path::new(output)
- );
- assert_eq!(
- sanitize_path(Path::new(input), false).unwrap(),
- Path::new(output)
- );
- }
-
- #[rstest]
- #[case(".foo")]
- #[case("/.foo")]
- #[case("foo/.bar/foo")]
- fn test_sanitize_path_no_hidden_files(#[case] input: &str) {
- assert_eq!(sanitize_path(Path::new(input), false), None);
- }
-}