Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File & directory deletion feature #1093

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions data/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ table thead th.date {
width: 21em;
}

table thead th.actions {
width: 4em;
}

table tbody tr:nth-child(odd) {
background: var(--odd_row_background);
}
Expand All @@ -277,6 +281,22 @@ td.date-cell {
justify-content: space-between;
}

td.actions-cell button {
padding: 0.1rem 0.3rem;
border-radius: 0.2rem;
border: none;
}

td.actions-cell .rm_form {
display: flex;
place-content: center;
}

td.actions-cell .rm_form button {
background: var(--rm_button_background);
color: var(--rm_button_text_color);
}
Comment on lines +295 to +298
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suck at visual design. So any styling improvement suggestion is welcomed.


.history {
color: var(--date_text_color);
}
Expand Down
2 changes: 2 additions & 0 deletions data/themes/archlinux.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ $generate_default: true !default;
--upload_form_background: #4b5162;
--upload_button_background: #ea95ff;
--upload_button_text_color: #ffffff;
--rm_button_background: #ea95ff;
--rm_button_text_color: #ffffff;
--drag_background: #3333338f;
--drag_border_color: #fefefe;
--drag_text_color: #fefefe;
Expand Down
2 changes: 2 additions & 0 deletions data/themes/monokai.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ $generate_default: true !default;
--upload_form_background: #49483e;
--upload_button_background: #ae81ff;
--upload_button_text_color: #f8f8f0;
--rm_button_background: #ae81ff;
--rm_button_text_color: #f8f8f0;
--drag_background: #3333338f;
--drag_border_color: #f8f8f2;
--drag_text_color: #f8f8f2;
Expand Down
2 changes: 2 additions & 0 deletions data/themes/squirrel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ $generate_default: true !default;
--upload_form_background: #f2f2f2;
--upload_button_background: #d02474;
--upload_button_text_color: #ffffff;
--rm_button_background: #d02474;
--rm_button_text_color: #ffffff;
--drag_background: #3333338f;
--drag_border_color: #ffffff;
--drag_text_color: #ffffff;
Expand Down
2 changes: 2 additions & 0 deletions data/themes/zenburn.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ $generate_default: true !default;
--upload_form_background: #777777;
--upload_button_background: #cc9393;
--upload_button_text_color: #efefef;
--rm_button_background: #cc9393;
--rm_button_text_color: #efefef;
--drag_background: #3333338f;
--drag_border_color: #efefef;
--drag_text_color: #efefef;
Expand Down
11 changes: 11 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,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::DirPath,
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 actix_web::http::header::HeaderMap;
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 @@ -257,15 +263,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 @@ -294,6 +299,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 @@ -311,3 +318,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()
}
5 changes: 5 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub enum RuntimeError {
#[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 Expand Up @@ -86,6 +90,7 @@ impl ResponseError for RuntimeError {
E::MultipartError(_) => S::BAD_REQUEST,
E::DuplicateFileError => S::CONFLICT,
E::UploadForbiddenError => S::FORBIDDEN,
E::RmForbiddenError => S::FORBIDDEN,
E::InvalidPathError(_) => S::BAD_REQUEST,
E::InsufficientPermissionsError(_) => S::FORBIDDEN,
E::ParseError(_, _) => S::BAD_REQUEST,
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::RuntimeError, file_utils::contains_symlink,
Expand Down Expand Up @@ -243,3 +245,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, RuntimeError> {
let conf = req.app_data::<MiniserveConfig>().unwrap();
let rm_path = sanitize_path(&query.path, conf.show_hidden).ok_or_else(|| {
RuntimeError::InvalidPathError("Invalid value for 'path' parameter".to_string())
})?;
let app_root_dir = conf.path.canonicalize().map_err(|e| {
RuntimeError::IoError("Failed to resolve path served by miniserve".to_string(), e)
})?;

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

if !rm_allowed {
return Err(RuntimeError::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(RuntimeError::InvalidHttpRequestError(
"Invalid value for 'path' parameter".to_string(),
)),
}?;

// Handle non-existent path
if !canonicalized_rm_path.exists() {
return Err(RuntimeError::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(RuntimeError::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())
}
23 changes: 1 addition & 22 deletions src/listing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,9 @@ use strum::{Display, EnumString};
use crate::archive::ArchiveMethod;
use crate::auth::CurrentUser;
use crate::errors::{self, RuntimeError};
use crate::path_utils::percent_encode_sets::COMPONENT;
use crate::renderer;

use self::percent_encode_sets::COMPONENT;

/// "percent-encode sets" as defined by WHATWG specs:
/// https://url.spec.whatwg.org/#percent-encoded-bytes
mod percent_encode_sets {
use percent_encoding::{AsciiSet, CONTROLS};
pub const QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>');
pub const PATH: &AsciiSet = &QUERY.add(b'?').add(b'`').add(b'{').add(b'}');
pub const USERINFO: &AsciiSet = &PATH
.add(b'/')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'|');
pub const COMPONENT: &AsciiSet = &USERINFO.add(b'$').add(b'%').add(b'&').add(b'+').add(b',');
}

/// Query parameters used by listing APIs
#[derive(Deserialize, Default)]
pub struct ListingQueryParameters {
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod errors;
mod file_op;
mod file_utils;
mod listing;
mod path_utils;
mod pipe;
mod renderer;

Expand Down Expand Up @@ -373,6 +374,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
19 changes: 19 additions & 0 deletions src/path_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// "percent-encode sets" as defined by WHATWG specs:
/// https://url.spec.whatwg.org/#percent-encoded-bytes
pub mod percent_encode_sets {
use percent_encoding::{AsciiSet, CONTROLS};
pub const QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>');
pub const PATH: &AsciiSet = &QUERY.add(b'?').add(b'`').add(b'{').add(b'}');
pub const USERINFO: &AsciiSet = &PATH
.add(b'/')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'|');
pub const COMPONENT: &AsciiSet = &USERINFO.add(b'$').add(b'%').add(b'&').add(b'+').add(b',');
}
Loading