diff --git a/Cargo.lock b/Cargo.lock index 40c1f3f..35cea9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bitflags" version = "1.3.2" @@ -49,9 +55,10 @@ dependencies = [ ] [[package]] -name = "duf2" +name = "duf" version = "0.1.0" dependencies = [ + "base64", "clap", "futures", "hyper", diff --git a/Cargo.toml b/Cargo.toml index a318849..bdc7595 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "duf2" +name = "duf" version = "0.1.0" edition = "2021" @@ -14,6 +14,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tokio-util = { version = "0.7", features = ["codec", "io-util"] } futures = "0.3" +base64 = "0.13.0" [dev-dependencies] tempfile = "3" diff --git a/src/args.rs b/src/args.rs index bae2811..4fdeba8 100644 --- a/src/args.rs +++ b/src/args.rs @@ -29,11 +29,24 @@ fn app() -> clap::Command<'static> { .allow_invalid_utf8(true) .help("Path to a directory for serving files"); + let arg_readonly = Arg::new("readonly") + .short('r') + .long("readonly") + .help("Only serve static files, no operations like upload and delete"); + + let arg_auth = Arg::new("auth") + .short('a') + .long("auth") + .help("Authenticate with user and pass") + .value_name("user:pass"); + clap::command!() .about(ABOUT) .arg(arg_address) .arg(arg_port) .arg(arg_path) + .arg(arg_readonly) + .arg(arg_auth) } pub fn matches() -> ArgMatches { @@ -45,6 +58,8 @@ pub struct Args { pub address: String, pub port: u16, pub path: PathBuf, + pub readonly: bool, + pub auth: Option, } impl Args { @@ -57,11 +72,15 @@ impl Args { let port = matches.value_of_t::("port")?; let path = matches.value_of_os("path").unwrap_or_default(); let path = Args::parse_path(path)?; + let readonly = matches.is_present("readonly"); + let auth = matches.value_of("auth").map(|v| v.to_owned()); Ok(Args { address, port, path, + readonly, + auth, }) } diff --git a/src/index.css b/src/index.css index d92d9c6..f59b195 100644 --- a/src/index.css +++ b/src/index.css @@ -44,7 +44,7 @@ html { padding-left: 0.5em; } -.action { +.upload-control { cursor: pointer; } diff --git a/src/index.html b/src/index.html index 2c67065..3174871 100644 --- a/src/index.html +++ b/src/index.html @@ -10,12 +10,6 @@
-
- - -
@@ -34,11 +28,12 @@
diff --git a/src/server.rs b/src/server.rs index 8433203..663a77e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,6 +1,7 @@ use crate::{Args, BoxResult}; use futures::TryStreamExt; +use hyper::header::HeaderValue; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, StatusCode}; use percent_encoding::percent_decode; @@ -62,6 +63,9 @@ impl InnerService { let res = if req.method() == Method::GET { self.handle_static(req).await } else if req.method() == Method::PUT { + if self.args.readonly { + return Ok(status_code!(StatusCode::FORBIDDEN)); + } self.handle_upload(req).await } else { return Ok(status_code!(StatusCode::NOT_FOUND)); @@ -70,6 +74,11 @@ impl InnerService { } async fn handle_static(&self, req: Request) -> BoxResult { + if !self.auth_guard(&req).unwrap_or_default() { + let mut res = status_code!(StatusCode::UNAUTHORIZED); + res.headers_mut().insert("WWW-Authenticate" , HeaderValue::from_static("Basic")); + return Ok(res) + } let path = match self.get_file_path(req.uri().path())? { Some(path) => path, None => return Ok(status_code!(StatusCode::FORBIDDEN)), @@ -87,6 +96,11 @@ impl InnerService { } async fn handle_upload(&self, mut req: Request) -> BoxResult { + if !self.auth_guard(&req).unwrap_or_default() { + let mut res = status_code!(StatusCode::UNAUTHORIZED); + res.headers_mut().insert("WWW-Authenticate" , HeaderValue::from_static("Basic")); + return Ok(res) + } let path = match self.get_file_path(req.uri().path())? { Some(path) => path, None => return Ok(status_code!(StatusCode::FORBIDDEN)), @@ -165,7 +179,7 @@ impl InnerService { paths.sort_unstable(); let breadcrumb = self.get_breadcrumb(path); - let data = SendDirData { breadcrumb, paths }; + let data = SendDirData { breadcrumb, paths, readonly: !self.args.readonly }; let data = serde_json::to_string(&data).unwrap(); let mut output = @@ -182,6 +196,25 @@ impl InnerService { Ok(Response::new(body)) } + fn auth_guard(&self, req: &Request) -> BoxResult { + if let Some(auth) = &self.args.auth { + if let Some(value) = req.headers().get("Authorization") { + let value = value.to_str()?; + let value = if value.contains("Basic ") { + &value[6..] + } else { + return Ok(false); + }; + let value = base64::decode(value)?; + let value = std::str::from_utf8(&value)?; + return Ok(value == auth); + } else { + return Ok(false); + } + } + Ok(true) + } + fn get_breadcrumb(&self, path: &Path) -> String { let path = match self.args.path.parent() { Some(p) => path.strip_prefix(p).unwrap(), @@ -210,6 +243,7 @@ impl InnerService { struct SendDirData { breadcrumb: String, paths: Vec, + readonly: bool, } #[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]