Skip to content

Commit

Permalink
Initial backend impl
Browse files Browse the repository at this point in the history
  • Loading branch information
cyqsimon committed Jan 26, 2024
1 parent 2fe13fb commit 8fef88b
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 12 deletions.
11 changes: 11 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,17 @@ pub struct CliArgs {
#[arg(short = 'o', long = "overwrite-files", env = "OVERWRITE_FILES")]
pub overwrite_files: bool,

/// Enable file and directory deletion (and optionally specify for which directory)
#[arg(
short = 'R',
long = "rm-files",
value_hint = ValueHint::FilePath,
num_args(0..=1),
value_delimiter(','),
env = "MINISERVE_ALLOWED_RM_DIR"
)]
pub allowed_rm_dir: Option<Vec<PathBuf>>,

/// Enable uncompressed tar archive generation
#[arg(short = 'r', long = "enable-tar", env = "MINISERVE_ENABLE_TAR")]
pub enable_tar: bool,
Expand Down
38 changes: 28 additions & 10 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{
fs::File,
io::{BufRead, BufReader},
net::{IpAddr, Ipv4Addr, Ipv6Addr},
path::PathBuf,
path::{Path, PathBuf},
};

use anyhow::{anyhow, Context, Result};
Expand Down Expand Up @@ -110,6 +110,12 @@ pub struct MiniserveConfig {
/// Enable upload to override existing files
pub overwrite_files: bool,

/// Enable file and directory deletion
pub rm_enabled: bool,

/// List of allowed deletion directories
pub allowed_rm_dir: Vec<String>,

/// If false, creation of uncompressed tar archives is disabled
pub tar_enabled: bool,

Expand Down Expand Up @@ -263,15 +269,14 @@ impl MiniserveConfig {
let allowed_upload_dir = args
.allowed_upload_dir
.as_ref()
.map(|v| {
v.iter()
.map(|p| {
sanitize_path(p, args.hidden)
.map(|p| p.display().to_string().replace('\\', "/"))
.ok_or(anyhow!("Illegal path {p:?}"))
})
.collect()
})
.map(|paths| validate_allowed_paths(paths, args.hidden))
.transpose()?
.unwrap_or_default();

let allowed_rm_dir = args
.allowed_rm_dir
.as_ref()
.map(|paths| validate_allowed_paths(paths, args.hidden))
.transpose()?
.unwrap_or_default();

Expand Down Expand Up @@ -300,6 +305,8 @@ impl MiniserveConfig {
file_upload: args.allowed_upload_dir.is_some(),
allowed_upload_dir,
uploadable_media_type,
rm_enabled: args.allowed_rm_dir.is_some(),
allowed_rm_dir,
tar_enabled: args.enable_tar,
tar_gz_enabled: args.enable_tar_gz,
zip_enabled: args.enable_zip,
Expand All @@ -316,3 +323,14 @@ impl MiniserveConfig {
})
}
}

fn validate_allowed_paths(paths: &[impl AsRef<Path>], allow_hidden: bool) -> Result<Vec<String>> {
paths
.iter()
.map(|p| {
sanitize_path(p, allow_hidden)
.map(|p| p.display().to_string().replace('\\', "/"))
.ok_or(anyhow!("Illegal path {:?}", p.as_ref()))
})
.collect()
}
4 changes: 4 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ pub enum ContextualError {
#[error("Upload not allowed to this directory")]
UploadForbiddenError,

/// Remove not allowed
#[error("Remove not allowed to this directory")]
RmForbiddenError,

/// Any error related to an invalid path (failed to retrieve entry name, unexpected entry type, etc)
#[error("Invalid path\ncaused by: {0}")]
InvalidPathError(String),
Expand Down
71 changes: 69 additions & 2 deletions src/file_op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ use actix_web::{http::header, web, HttpRequest, HttpResponse};
use futures::TryFutureExt;
use futures::TryStreamExt;
use serde::Deserialize;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tokio::{
fs::{self, File},
io::AsyncWriteExt,
};

use crate::{
config::MiniserveConfig, errors::ContextualError, file_utils::contains_symlink,
Expand Down Expand Up @@ -245,3 +247,68 @@ pub async fn upload_file(
.append_header((header::LOCATION, return_path))
.finish())
}

/// Handle incoming request to remove a file or 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.
pub async fn rm_file(
req: HttpRequest,
query: web::Query<FileOpQueryParameters>,
) -> Result<HttpResponse, ContextualError> {
let conf = req.app_data::<MiniserveConfig>().unwrap();
let rm_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)
})?;

// Disallow paths outside of allowed directories
let rm_allowed = conf.allowed_upload_dir.is_empty()
|| conf.allowed_rm_dir.iter().any(|s| rm_path.starts_with(s));

if !rm_allowed {
return Err(ContextualError::RmForbiddenError);
}

// Disallow the target path to go outside of the served directory
let canonicalized_rm_path = match app_root_dir.join(&rm_path).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(),
)),
}?;

// Handle non-existent path
if !canonicalized_rm_path.exists() {
return Err(ContextualError::RouteNotFoundError(format!(
"{rm_path:?} does not exist"
)));
}

// Remove
let rm_res = if canonicalized_rm_path.is_dir() {
fs::remove_dir_all(&canonicalized_rm_path).await
} else {
fs::remove_file(&canonicalized_rm_path).await
};
if let Err(err) = rm_res {
Err(ContextualError::IoError(
format!("Failed to remove {rm_path:?}"),
err,
))?;
}

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())
}
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
// Allow file upload
app.service(web::resource("/upload").route(web::post().to(file_op::upload_file)));
}
if conf.rm_enabled {
// Allow file and directory deletion
app.service(web::resource("/rm").route(web::post().to(file_op::rm_file)));
}
// Handle directories
app.service(dir_service());
}
Expand Down

0 comments on commit 8fef88b

Please sign in to comment.