aboutsummaryrefslogtreecommitdiffstats
path: root/src/file_op.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/file_op.rs (renamed from src/file_upload.rs)152
1 files changed, 44 insertions, 108 deletions
diff --git a/src/file_upload.rs b/src/file_op.rs
index 2275c73..d9786c4 100644
--- a/src/file_upload.rs
+++ b/src/file_op.rs
@@ -1,13 +1,18 @@
+//! Handlers for file upload and removal
+
use std::{
io::Write,
path::{Component, Path, PathBuf},
};
-use actix_web::{http::header, HttpRequest, HttpResponse};
+use actix_web::{http::header, web, HttpRequest, HttpResponse};
use futures::TryStreamExt;
+use serde::Deserialize;
-use crate::errors::ContextualError;
-use crate::listing;
+use crate::{
+ config::MiniserveConfig, errors::ContextualError, file_utils::contains_symlink,
+ file_utils::sanitize_path,
+};
/// Saves file data from a multipart form field (`field`) to `file_path`, optionally overwriting
/// existing file.
@@ -110,10 +115,16 @@ async fn handle_multipart(
})?;
// Ensure there are no illegal symlinks
- if !allow_symlinks && contains_symlink(&absolute_path) {
- return Err(ContextualError::InsufficientPermissionsError(
- user_given_path.display().to_string(),
- ));
+ if !allow_symlinks {
+ match contains_symlink(&absolute_path) {
+ Err(err) => Err(ContextualError::InsufficientPermissionsError(
+ err.to_string(),
+ ))?,
+ Ok(true) => Err(ContextualError::InsufficientPermissionsError(format!(
+ "{user_given_path:?} traverses through a symlink"
+ )))?,
+ Ok(false) => (),
+ }
}
std::fs::create_dir_all(&absolute_path).map_err(|e| {
@@ -135,15 +146,27 @@ async fn handle_multipart(
})?;
// Ensure there are no illegal symlinks in the file upload path
- if !allow_symlinks && contains_symlink(&path) {
- return Err(ContextualError::InsufficientPermissionsError(
- filename.to_string(),
- ));
+ if !allow_symlinks {
+ match contains_symlink(&path) {
+ Err(err) => Err(ContextualError::InsufficientPermissionsError(
+ err.to_string(),
+ ))?,
+ Ok(true) => Err(ContextualError::InsufficientPermissionsError(format!(
+ "{path:?} traverses through a symlink"
+ )))?,
+ Ok(false) => (),
+ }
}
save_file(field, path.join(filename_path), overwrite_files).await
}
+/// Query parameters used by upload and rm APIs
+#[derive(Deserialize, Default)]
+pub struct FileOpQueryParameters {
+ path: PathBuf,
+}
+
/// 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
@@ -151,23 +174,13 @@ async fn handle_multipart(
/// This method returns future.
pub async fn upload_file(
req: HttpRequest,
- payload: actix_web::web::Payload,
+ query: web::Query<FileOpQueryParameters>,
+ payload: 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(|| {
+ let conf = req.app_data::<MiniserveConfig>().unwrap();
+ let upload_path = sanitize_path(&query.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)
})?;
@@ -210,90 +223,13 @@ pub async fn upload_file(
.try_collect::<Vec<u64>>()
.await?;
+ let return_path = req
+ .headers()
+ .get(header::REFERER)
+ .and_then(|h| h.to_str().ok())
+ .unwrap_or("/");
+
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);
- }
-}