feat: add basic auth and readonly mode
This commit is contained in:
parent
1c97c01096
commit
78e7daf7cc
6 changed files with 90 additions and 20 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -8,6 +8,12 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
|
@ -49,9 +55,10 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "duf2"
|
name = "duf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "duf2"
|
name = "duf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio-util = { version = "0.7", features = ["codec", "io-util"] }
|
tokio-util = { version = "0.7", features = ["codec", "io-util"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
base64 = "0.13.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
19
src/args.rs
19
src/args.rs
|
@ -29,11 +29,24 @@ fn app() -> clap::Command<'static> {
|
||||||
.allow_invalid_utf8(true)
|
.allow_invalid_utf8(true)
|
||||||
.help("Path to a directory for serving files");
|
.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!()
|
clap::command!()
|
||||||
.about(ABOUT)
|
.about(ABOUT)
|
||||||
.arg(arg_address)
|
.arg(arg_address)
|
||||||
.arg(arg_port)
|
.arg(arg_port)
|
||||||
.arg(arg_path)
|
.arg(arg_path)
|
||||||
|
.arg(arg_readonly)
|
||||||
|
.arg(arg_auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn matches() -> ArgMatches {
|
pub fn matches() -> ArgMatches {
|
||||||
|
@ -45,6 +58,8 @@ pub struct Args {
|
||||||
pub address: String,
|
pub address: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
pub readonly: bool,
|
||||||
|
pub auth: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
|
@ -57,11 +72,15 @@ impl Args {
|
||||||
let port = matches.value_of_t::<u16>("port")?;
|
let port = matches.value_of_t::<u16>("port")?;
|
||||||
let path = matches.value_of_os("path").unwrap_or_default();
|
let path = matches.value_of_os("path").unwrap_or_default();
|
||||||
let path = Args::parse_path(path)?;
|
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 {
|
Ok(Args {
|
||||||
address,
|
address,
|
||||||
port,
|
port,
|
||||||
path,
|
path,
|
||||||
|
readonly,
|
||||||
|
auth,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ html {
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.upload-control {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,6 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<div class="breadcrumb"></div>
|
<div class="breadcrumb"></div>
|
||||||
<div class="action" title="Upload file">
|
|
||||||
<label for="file">
|
|
||||||
<svg viewBox="0 0 384.97 384.97" width="14" height="14"><path d="M372.939,264.641c-6.641,0-12.03,5.39-12.03,12.03v84.212H24.061v-84.212c0-6.641-5.39-12.03-12.03-12.03 S0,270.031,0,276.671v96.242c0,6.641,5.39,12.03,12.03,12.03h360.909c6.641,0,12.03-5.39,12.03-12.03v-96.242 C384.97,270.019,379.58,264.641,372.939,264.641z"></path><path d="M117.067,103.507l63.46-62.558v235.71c0,6.641,5.438,12.03,12.151,12.03c6.713,0,12.151-5.39,12.151-12.03V40.95 l63.46,62.558c4.74,4.704,12.439,4.704,17.179,0c4.74-4.704,4.752-12.319,0-17.011l-84.2-82.997 c-4.692-4.656-12.584-4.608-17.191,0L99.888,86.496c-4.752,4.704-4.74,12.319,0,17.011 C104.628,108.211,112.327,108.211,117.067,103.507z"></path></svg>
|
|
||||||
</label>
|
|
||||||
<input type="file" id="file" name="file" multiple>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="uploaders">
|
<div class="uploaders">
|
||||||
|
@ -34,11 +28,12 @@
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
const $head = document.querySelector(".head");
|
||||||
const $tbody = document.querySelector(".main tbody");
|
const $tbody = document.querySelector(".main tbody");
|
||||||
const $breadcrumb = document.querySelector(".breadcrumb");
|
const $breadcrumb = document.querySelector(".breadcrumb");
|
||||||
const $fileInput = document.getElementById("file");
|
|
||||||
const $uploaders = document.querySelector(".uploaders");
|
const $uploaders = document.querySelector(".uploaders");
|
||||||
const { breadcrumb, paths } = __DATA__;
|
const $uploadControl = document.querySelector(".upload-control");
|
||||||
|
const { breadcrumb, paths, readonly } = __DATA__;
|
||||||
let uploaderIdx = 0;
|
let uploaderIdx = 0;
|
||||||
|
|
||||||
class Uploader {
|
class Uploader {
|
||||||
|
@ -121,6 +116,17 @@
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addUploadControl() {
|
||||||
|
$head.insertAdjacentHTML("beforeend", `
|
||||||
|
<div class="upload-control" title="Upload file">
|
||||||
|
<label for="file">
|
||||||
|
<svg viewBox="0 0 384.97 384.97" width="14" height="14"><path d="M372.939,264.641c-6.641,0-12.03,5.39-12.03,12.03v84.212H24.061v-84.212c0-6.641-5.39-12.03-12.03-12.03 S0,270.031,0,276.671v96.242c0,6.641,5.39,12.03,12.03,12.03h360.909c6.641,0,12.03-5.39,12.03-12.03v-96.242 C384.97,270.019,379.58,264.641,372.939,264.641z"></path><path d="M117.067,103.507l63.46-62.558v235.71c0,6.641,5.438,12.03,12.151,12.03c6.713,0,12.151-5.39,12.151-12.03V40.95 l63.46,62.558c4.74,4.704,12.439,4.704,17.179,0c4.74-4.704,4.752-12.319,0-17.011l-84.2-82.997 c-4.692-4.656-12.584-4.608-17.191,0L99.888,86.496c-4.752,4.704-4.74,12.319,0,17.011 C104.628,108.211,112.327,108.211,117.067,103.507z"></path></svg>
|
||||||
|
</label>
|
||||||
|
<input type="file" id="file" name="file" multiple>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
function getSvg(path_type) {
|
function getSvg(path_type) {
|
||||||
switch (path_type) {
|
switch (path_type) {
|
||||||
case "Dir":
|
case "Dir":
|
||||||
|
@ -161,14 +167,17 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
addBreadcrumb(breadcrumb);
|
addBreadcrumb(breadcrumb);
|
||||||
paths.forEach(file => addFile(file));
|
paths.forEach(file => addFile(file));
|
||||||
$fileInput.addEventListener("change", e => {
|
if (readonly) {
|
||||||
const files = e.target.files;
|
addUploadControl();
|
||||||
for (const file of files) {
|
document.getElementById("file").addEventListener("change", e => {
|
||||||
uploaderIdx += 1;
|
const files = e.target.files;
|
||||||
const uploader = new Uploader(uploaderIdx, file);
|
for (const file of files) {
|
||||||
uploader.upload();
|
uploaderIdx += 1;
|
||||||
}
|
const uploader = new Uploader(uploaderIdx, file);
|
||||||
});
|
uploader.upload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{Args, BoxResult};
|
use crate::{Args, BoxResult};
|
||||||
|
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
use hyper::header::HeaderValue;
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use hyper::{Body, Method, StatusCode};
|
use hyper::{Body, Method, StatusCode};
|
||||||
use percent_encoding::percent_decode;
|
use percent_encoding::percent_decode;
|
||||||
|
@ -62,6 +63,9 @@ impl InnerService {
|
||||||
let res = if req.method() == Method::GET {
|
let res = if req.method() == Method::GET {
|
||||||
self.handle_static(req).await
|
self.handle_static(req).await
|
||||||
} else if req.method() == Method::PUT {
|
} else if req.method() == Method::PUT {
|
||||||
|
if self.args.readonly {
|
||||||
|
return Ok(status_code!(StatusCode::FORBIDDEN));
|
||||||
|
}
|
||||||
self.handle_upload(req).await
|
self.handle_upload(req).await
|
||||||
} else {
|
} else {
|
||||||
return Ok(status_code!(StatusCode::NOT_FOUND));
|
return Ok(status_code!(StatusCode::NOT_FOUND));
|
||||||
|
@ -70,6 +74,11 @@ impl InnerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_static(&self, req: Request) -> BoxResult<Response> {
|
async fn handle_static(&self, req: Request) -> BoxResult<Response> {
|
||||||
|
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())? {
|
let path = match self.get_file_path(req.uri().path())? {
|
||||||
Some(path) => path,
|
Some(path) => path,
|
||||||
None => return Ok(status_code!(StatusCode::FORBIDDEN)),
|
None => return Ok(status_code!(StatusCode::FORBIDDEN)),
|
||||||
|
@ -87,6 +96,11 @@ impl InnerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_upload(&self, mut req: Request) -> BoxResult<Response> {
|
async fn handle_upload(&self, mut req: Request) -> BoxResult<Response> {
|
||||||
|
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())? {
|
let path = match self.get_file_path(req.uri().path())? {
|
||||||
Some(path) => path,
|
Some(path) => path,
|
||||||
None => return Ok(status_code!(StatusCode::FORBIDDEN)),
|
None => return Ok(status_code!(StatusCode::FORBIDDEN)),
|
||||||
|
@ -165,7 +179,7 @@ impl InnerService {
|
||||||
|
|
||||||
paths.sort_unstable();
|
paths.sort_unstable();
|
||||||
let breadcrumb = self.get_breadcrumb(path);
|
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 data = serde_json::to_string(&data).unwrap();
|
||||||
|
|
||||||
let mut output =
|
let mut output =
|
||||||
|
@ -182,6 +196,25 @@ impl InnerService {
|
||||||
Ok(Response::new(body))
|
Ok(Response::new(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auth_guard(&self, req: &Request) -> BoxResult<bool> {
|
||||||
|
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 {
|
fn get_breadcrumb(&self, path: &Path) -> String {
|
||||||
let path = match self.args.path.parent() {
|
let path = match self.args.path.parent() {
|
||||||
Some(p) => path.strip_prefix(p).unwrap(),
|
Some(p) => path.strip_prefix(p).unwrap(),
|
||||||
|
@ -210,6 +243,7 @@ impl InnerService {
|
||||||
struct SendDirData {
|
struct SendDirData {
|
||||||
breadcrumb: String,
|
breadcrumb: String,
|
||||||
paths: Vec<PathItem>,
|
paths: Vec<PathItem>,
|
||||||
|
readonly: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
|
Loading…
Reference in a new issue