mirror of
https://github.com/sigoden/dufs.git
synced 2026-04-09 09:09:03 +03:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7481db5071 | ||
|
|
b0cc901416 | ||
|
|
ce154d9ebc | ||
|
|
7c4c264206 | ||
|
|
c1e0c6bb2f | ||
|
|
f138915f20 | ||
|
|
a0b413ef30 | ||
|
|
fc13d41c17 | ||
|
|
882a9ae716 | ||
|
|
5578ee9190 | ||
|
|
916602ae2d | ||
|
|
2f40313a54 | ||
|
|
05155aa532 |
29
CHANGELOG.md
29
CHANGELOG.md
@@ -2,6 +2,35 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.13.2] - 2022-06-06
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Filename xml escaping
|
||||
- Escape path-prefix/url-prefix different
|
||||
|
||||
## [0.13.1] - 2022-06-05
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Escape filename ([#21](https://github.com/sigoden/duf/issues/21))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use logger ([#22](https://github.com/sigoden/duf/issues/22))
|
||||
|
||||
## [0.13.0] - 2022-06-05
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Ctrl+c not exit sometimes
|
||||
|
||||
### Features
|
||||
|
||||
- Implement more webdav methods ([#13](https://github.com/sigoden/duf/issues/13))
|
||||
- Use digest auth ([#14](https://github.com/sigoden/duf/issues/14))
|
||||
- Add webdav proppatch handler ([#18](https://github.com/sigoden/duf/issues/18))
|
||||
|
||||
## [0.12.1] - 2022-06-04
|
||||
|
||||
### Features
|
||||
|
||||
100
Cargo.lock
generated
100
Cargo.lock
generated
@@ -286,17 +286,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "duf"
|
||||
version = "0.12.1"
|
||||
version = "0.13.2"
|
||||
dependencies = [
|
||||
"async-walkdir",
|
||||
"async_zip",
|
||||
"base64",
|
||||
"chrono",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"get_if_addrs",
|
||||
"headers",
|
||||
"hyper",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"md5",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
@@ -307,6 +311,19 @@ dependencies = [
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
|
||||
dependencies = [
|
||||
"humantime",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -484,6 +501,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
@@ -558,6 +586,12 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.19"
|
||||
@@ -656,6 +690,12 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
@@ -770,6 +810,12 @@ version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.39"
|
||||
@@ -788,6 +834,36 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
@@ -1098,6 +1174,22 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
@@ -1277,6 +1369,12 @@ version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.6"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "duf"
|
||||
version = "0.12.1"
|
||||
version = "0.13.2"
|
||||
edition = "2021"
|
||||
authors = ["sigoden <sigoden@gmail.com>"]
|
||||
description = "Duf is a simple file server."
|
||||
@@ -31,6 +31,13 @@ mime_guess = "2.0.4"
|
||||
get_if_addrs = "0.5.3"
|
||||
rustls = { version = "0.20", default-features = false, features = ["tls12"] }
|
||||
rustls-pemfile = "1"
|
||||
md5 = "0.7.0"
|
||||
lazy_static = "1.4.0"
|
||||
uuid = { version = "1.1.1", features = ["v4", "fast-rng"] }
|
||||
urlencoding = "2.1.0"
|
||||
xml-rs = "0.8"
|
||||
env_logger = { version = "0.9.0", default-features = false, features = ["humantime"] }
|
||||
log = "0.4.17"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
@@ -5,6 +5,6 @@ WORKDIR /app
|
||||
COPY . .
|
||||
RUN cargo build --target x86_64-unknown-linux-musl --release
|
||||
|
||||
FROM alpine
|
||||
FROM scratch
|
||||
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/duf /bin/
|
||||
ENTRYPOINT ["/bin/duf"]
|
||||
@@ -3,7 +3,7 @@
|
||||
[](https://github.com/sigoden/duf/actions/workflows/ci.yaml)
|
||||
[](https://crates.io/crates/duf)
|
||||
|
||||
Duf is a simple file server. Support static serve, search, upload, delete, webdav...
|
||||
Duf is a simple file server. Support static serve, search, upload, webdav...
|
||||
|
||||

|
||||
|
||||
@@ -11,11 +11,10 @@ Duf is a simple file server. Support static serve, search, upload, delete, webda
|
||||
|
||||
- Serve static files
|
||||
- Download folder as zip file
|
||||
- Search files
|
||||
- Upload files and folders (Drag & Drop)
|
||||
- Delete files
|
||||
- Basic authentication
|
||||
- Search files
|
||||
- Partial responses (Parallel/Resume download)
|
||||
- Authentication
|
||||
- Support https
|
||||
- Support webdav
|
||||
- Easy to use with curl
|
||||
@@ -111,8 +110,6 @@ duf --tls-cert my.crt --tls-key my.key
|
||||
Download a file
|
||||
```
|
||||
curl http://127.0.0.1:5000/some-file
|
||||
|
||||
curl -o some-file2 http://127.0.0.1:5000/some-file
|
||||
```
|
||||
|
||||
Download a folder as zip file
|
||||
|
||||
@@ -87,7 +87,7 @@ class Uploader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add breadcumb
|
||||
* Add breadcrumb
|
||||
* @param {string} value
|
||||
*/
|
||||
function addBreadcrumb(value) {
|
||||
|
||||
20
src/args.rs
20
src/args.rs
@@ -5,6 +5,7 @@ use std::net::SocketAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs, io};
|
||||
|
||||
use crate::auth::parse_auth;
|
||||
use crate::BoxResult;
|
||||
|
||||
const ABOUT: &str = concat!("\n", crate_description!()); // Add extra newline.
|
||||
@@ -113,8 +114,9 @@ pub struct Args {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub path: PathBuf,
|
||||
pub path_prefix: Option<String>,
|
||||
pub auth: Option<String>,
|
||||
pub path_prefix: String,
|
||||
pub uri_prefix: String,
|
||||
pub auth: Option<(String, String)>,
|
||||
pub no_auth_access: bool,
|
||||
pub allow_upload: bool,
|
||||
pub allow_delete: bool,
|
||||
@@ -136,9 +138,18 @@ impl Args {
|
||||
let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
|
||||
let path_prefix = matches
|
||||
.value_of("path-prefix")
|
||||
.map(|v| v.trim_matches('/').to_owned());
|
||||
.map(|v| v.trim_matches('/').to_owned())
|
||||
.unwrap_or_default();
|
||||
let uri_prefix = if path_prefix.is_empty() {
|
||||
"/".to_owned()
|
||||
} else {
|
||||
format!("/{}/", &path_prefix)
|
||||
};
|
||||
let cors = matches.is_present("cors");
|
||||
let auth = matches.value_of("auth").map(|v| v.to_owned());
|
||||
let auth = match matches.value_of("auth") {
|
||||
Some(auth) => Some(parse_auth(auth)?),
|
||||
None => None,
|
||||
};
|
||||
let no_auth_access = matches.is_present("no-auth-access");
|
||||
let allow_upload = matches.is_present("allow-all") || matches.is_present("allow-upload");
|
||||
let allow_delete = matches.is_present("allow-all") || matches.is_present("allow-delete");
|
||||
@@ -159,6 +170,7 @@ impl Args {
|
||||
port,
|
||||
path,
|
||||
path_prefix,
|
||||
uri_prefix,
|
||||
auth,
|
||||
no_auth_access,
|
||||
cors,
|
||||
|
||||
209
src/auth.rs
Normal file
209
src/auth.rs
Normal file
@@ -0,0 +1,209 @@
|
||||
use headers::HeaderValue;
|
||||
use lazy_static::lazy_static;
|
||||
use md5::Context;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::BoxResult;
|
||||
|
||||
const REALM: &str = "DUF";
|
||||
|
||||
lazy_static! {
|
||||
static ref NONCESTARTHASH: Context = {
|
||||
let mut h = Context::new();
|
||||
h.consume(Uuid::new_v4().as_bytes());
|
||||
h.consume(std::process::id().to_be_bytes());
|
||||
h
|
||||
};
|
||||
}
|
||||
|
||||
pub fn generate_www_auth(stale: bool) -> String {
|
||||
let str_stale = if stale { "stale=true," } else { "" };
|
||||
format!(
|
||||
"Digest realm=\"{}\",nonce=\"{}\",{}qop=\"auth\",algorithm=\"MD5\"",
|
||||
REALM,
|
||||
create_nonce(),
|
||||
str_stale
|
||||
)
|
||||
}
|
||||
|
||||
pub fn parse_auth(auth: &str) -> BoxResult<(String, String)> {
|
||||
let p: Vec<&str> = auth.trim().split(':').collect();
|
||||
let err = "Invalid auth value";
|
||||
if p.len() != 2 {
|
||||
return Err(err.into());
|
||||
}
|
||||
let user = p[0];
|
||||
let pass = p[1];
|
||||
let mut h = Context::new();
|
||||
h.consume(format!("{}:{}:{}", user, REALM, pass).as_bytes());
|
||||
Ok((user.to_owned(), format!("{:x}", h.compute())))
|
||||
}
|
||||
|
||||
pub fn valid_digest(
|
||||
header_value: &HeaderValue,
|
||||
method: &str,
|
||||
auth_user: &str,
|
||||
auth_pass: &str,
|
||||
) -> Option<()> {
|
||||
let digest_value = strip_prefix(header_value.as_bytes(), b"Digest ")?;
|
||||
let user_vals = to_headermap(digest_value).ok()?;
|
||||
if let (Some(username), Some(nonce), Some(user_response)) = (
|
||||
user_vals
|
||||
.get(b"username".as_ref())
|
||||
.and_then(|b| std::str::from_utf8(*b).ok()),
|
||||
user_vals.get(b"nonce".as_ref()),
|
||||
user_vals.get(b"response".as_ref()),
|
||||
) {
|
||||
match validate_nonce(nonce) {
|
||||
Ok(true) => {}
|
||||
_ => return None,
|
||||
}
|
||||
if auth_user != username {
|
||||
return None;
|
||||
}
|
||||
let mut ha = Context::new();
|
||||
ha.consume(method);
|
||||
ha.consume(b":");
|
||||
if let Some(uri) = user_vals.get(b"uri".as_ref()) {
|
||||
ha.consume(uri);
|
||||
}
|
||||
let ha = format!("{:x}", ha.compute());
|
||||
let mut correct_response = None;
|
||||
if let Some(qop) = user_vals.get(b"qop".as_ref()) {
|
||||
if qop == &b"auth".as_ref() || qop == &b"auth-int".as_ref() {
|
||||
correct_response = Some({
|
||||
let mut c = Context::new();
|
||||
c.consume(&auth_pass);
|
||||
c.consume(b":");
|
||||
c.consume(nonce);
|
||||
c.consume(b":");
|
||||
if let Some(nc) = user_vals.get(b"nc".as_ref()) {
|
||||
c.consume(nc);
|
||||
}
|
||||
c.consume(b":");
|
||||
if let Some(cnonce) = user_vals.get(b"cnonce".as_ref()) {
|
||||
c.consume(cnonce);
|
||||
}
|
||||
c.consume(b":");
|
||||
c.consume(qop);
|
||||
c.consume(b":");
|
||||
c.consume(&*ha);
|
||||
format!("{:x}", c.compute())
|
||||
});
|
||||
}
|
||||
}
|
||||
let correct_response = match correct_response {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
let mut c = Context::new();
|
||||
c.consume(&auth_pass);
|
||||
c.consume(b":");
|
||||
c.consume(nonce);
|
||||
c.consume(b":");
|
||||
c.consume(&*ha);
|
||||
format!("{:x}", c.compute())
|
||||
}
|
||||
};
|
||||
if correct_response.as_bytes() == *user_response {
|
||||
// grant access
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if a nonce is still valid.
|
||||
/// Return an error if it was never valid
|
||||
fn validate_nonce(nonce: &[u8]) -> Result<bool, ()> {
|
||||
if nonce.len() != 34 {
|
||||
return Err(());
|
||||
}
|
||||
//parse hex
|
||||
if let Ok(n) = std::str::from_utf8(nonce) {
|
||||
//get time
|
||||
if let Ok(secs_nonce) = u32::from_str_radix(&n[..8], 16) {
|
||||
//check time
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let secs_now = now.as_secs() as u32;
|
||||
|
||||
if let Some(dur) = secs_now.checked_sub(secs_nonce) {
|
||||
//check hash
|
||||
let mut h = NONCESTARTHASH.clone();
|
||||
h.consume(secs_nonce.to_be_bytes());
|
||||
let h = format!("{:x}", h.compute());
|
||||
if h[..26] == n[8..34] {
|
||||
return Ok(dur < 300); // from the last 5min
|
||||
//Authentication-Info ?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
|
||||
fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
|
||||
let l = prefix.len();
|
||||
if search.len() < l {
|
||||
return None;
|
||||
}
|
||||
if &search[..l] == prefix {
|
||||
Some(&search[l..])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_headermap(header: &[u8]) -> Result<HashMap<&[u8], &[u8]>, ()> {
|
||||
let mut sep = Vec::new();
|
||||
let mut asign = Vec::new();
|
||||
let mut i: usize = 0;
|
||||
let mut esc = false;
|
||||
for c in header {
|
||||
match (c, esc) {
|
||||
(b'=', false) => asign.push(i),
|
||||
(b',', false) => sep.push(i),
|
||||
(b'"', false) => esc = true,
|
||||
(b'"', true) => esc = false,
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
sep.push(i); // same len for both Vecs
|
||||
|
||||
i = 0;
|
||||
let mut ret = HashMap::new();
|
||||
for (&k, &a) in sep.iter().zip(asign.iter()) {
|
||||
while header[i] == b' ' {
|
||||
i += 1;
|
||||
}
|
||||
if a <= i || k <= 1 + a {
|
||||
//keys and vals must contain one char
|
||||
return Err(());
|
||||
}
|
||||
let key = &header[i..a];
|
||||
let val = if header[1 + a] == b'"' && header[k - 1] == b'"' {
|
||||
//escaped
|
||||
&header[2 + a..k - 1]
|
||||
} else {
|
||||
//not escaped
|
||||
&header[1 + a..k]
|
||||
};
|
||||
i = 1 + k;
|
||||
ret.insert(key, val);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn create_nonce() -> String {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
let secs = now.as_secs() as u32;
|
||||
let mut h = NONCESTARTHASH.clone();
|
||||
h.consume(secs.to_be_bytes());
|
||||
|
||||
let n = format!("{:08x}{:032x}", secs, h.compute());
|
||||
n[..34].to_string()
|
||||
}
|
||||
32
src/main.rs
32
src/main.rs
@@ -1,8 +1,15 @@
|
||||
mod args;
|
||||
mod auth;
|
||||
mod server;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub type BoxResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::args::{matches, Args};
|
||||
use crate::server::serve;
|
||||
|
||||
@@ -12,11 +19,34 @@ async fn main() {
|
||||
}
|
||||
|
||||
async fn run() -> BoxResult<()> {
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
env::set_var("RUST_LOG", "info")
|
||||
}
|
||||
env_logger::builder()
|
||||
.format(|buf, record| {
|
||||
let timestamp = buf.timestamp();
|
||||
writeln!(buf, "[{} {}] {}", timestamp, record.level(), record.args())
|
||||
})
|
||||
.init();
|
||||
|
||||
let args = Args::parse(matches())?;
|
||||
serve(args).await
|
||||
tokio::select! {
|
||||
ret = serve(args) => {
|
||||
ret
|
||||
},
|
||||
_ = shutdown_signal() => {
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_err<T>(err: Box<dyn std::error::Error>) -> T {
|
||||
eprintln!("error: {}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to install CTRL+C signal handler")
|
||||
}
|
||||
|
||||
381
src/server.rs
381
src/server.rs
@@ -1,9 +1,11 @@
|
||||
use crate::auth::{generate_www_auth, valid_digest};
|
||||
use crate::{Args, BoxResult};
|
||||
use xml::escape::escape_str_pcdata;
|
||||
|
||||
use async_walkdir::WalkDir;
|
||||
use async_zip::write::{EntryOptions, ZipFileWriter};
|
||||
use async_zip::Compression;
|
||||
use chrono::{Local, TimeZone, Utc};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use futures::stream::StreamExt;
|
||||
use futures::TryStreamExt;
|
||||
use get_if_addrs::get_if_addrs;
|
||||
@@ -34,6 +36,7 @@ use tokio::{fs, io};
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
use tokio_util::io::{ReaderStream, StreamReader};
|
||||
use uuid::Uuid;
|
||||
|
||||
type Request = hyper::Request<Body>;
|
||||
type Response = hyper::Response<Body>;
|
||||
@@ -52,65 +55,55 @@ macro_rules! status {
|
||||
}
|
||||
|
||||
pub async fn serve(args: Args) -> BoxResult<()> {
|
||||
match args.tls.as_ref() {
|
||||
Some(_) => serve_https(args).await,
|
||||
None => serve_http(args).await,
|
||||
let args = Arc::new(args);
|
||||
let socket_addr = args.address()?;
|
||||
let inner = Arc::new(InnerService::new(args.clone()));
|
||||
match args.tls.clone() {
|
||||
Some((certs, key)) => {
|
||||
let config = ServerConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)?;
|
||||
let tls_acceptor = TlsAcceptor::from(Arc::new(config));
|
||||
let arc_acceptor = Arc::new(tls_acceptor);
|
||||
let listener = TcpListener::bind(&socket_addr).await?;
|
||||
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
|
||||
let incoming =
|
||||
hyper::server::accept::from_stream(incoming.filter_map(|socket| async {
|
||||
match socket {
|
||||
Ok(stream) => match arc_acceptor.clone().accept(stream).await {
|
||||
Ok(val) => Some(Ok::<_, Infallible>(val)),
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
}
|
||||
}));
|
||||
let server = hyper::Server::builder(incoming).serve(make_service_fn(move |_| {
|
||||
let inner = inner.clone();
|
||||
async move {
|
||||
Ok::<_, Infallible>(service_fn(move |req| {
|
||||
let inner = inner.clone();
|
||||
inner.call(req)
|
||||
}))
|
||||
}
|
||||
}));
|
||||
print_listening(args.address.as_str(), args.port, &args.uri_prefix, true);
|
||||
server.await?;
|
||||
}
|
||||
None => {
|
||||
let server = hyper::Server::try_bind(&socket_addr)?.serve(make_service_fn(move |_| {
|
||||
let inner = inner.clone();
|
||||
async move {
|
||||
Ok::<_, Infallible>(service_fn(move |req| {
|
||||
let inner = inner.clone();
|
||||
inner.call(req)
|
||||
}))
|
||||
}
|
||||
}));
|
||||
print_listening(args.address.as_str(), args.port, &args.uri_prefix, false);
|
||||
server.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve_https(args: Args) -> BoxResult<()> {
|
||||
let args = Arc::new(args);
|
||||
let socket_addr = args.address()?;
|
||||
let (certs, key) = args.tls.clone().unwrap();
|
||||
let inner = Arc::new(InnerService::new(args.clone()));
|
||||
let config = ServerConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)?;
|
||||
let tls_acceptor = TlsAcceptor::from(Arc::new(config));
|
||||
let arc_acceptor = Arc::new(tls_acceptor);
|
||||
let listener = TcpListener::bind(&socket_addr).await?;
|
||||
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
|
||||
let incoming = hyper::server::accept::from_stream(incoming.filter_map(|socket| async {
|
||||
match socket {
|
||||
Ok(stream) => match arc_acceptor.clone().accept(stream).await {
|
||||
Ok(val) => Some(Ok::<_, Infallible>(val)),
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
}
|
||||
}));
|
||||
let server = hyper::Server::builder(incoming).serve(make_service_fn(move |_| {
|
||||
let inner = inner.clone();
|
||||
async move {
|
||||
Ok::<_, Infallible>(service_fn(move |req| {
|
||||
let inner = inner.clone();
|
||||
inner.call(req)
|
||||
}))
|
||||
}
|
||||
}));
|
||||
print_listening(args.address.as_str(), args.port, true);
|
||||
let graceful = server.with_graceful_shutdown(shutdown_signal());
|
||||
graceful.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn serve_http(args: Args) -> BoxResult<()> {
|
||||
let args = Arc::new(args);
|
||||
let socket_addr = args.address()?;
|
||||
let inner = Arc::new(InnerService::new(args.clone()));
|
||||
let server = hyper::Server::try_bind(&socket_addr)?.serve(make_service_fn(move |_| {
|
||||
let inner = inner.clone();
|
||||
async move {
|
||||
Ok::<_, Infallible>(service_fn(move |req| {
|
||||
let inner = inner.clone();
|
||||
inner.call(req)
|
||||
}))
|
||||
}
|
||||
}));
|
||||
print_listening(args.address.as_str(), args.port, false);
|
||||
let graceful = server.with_graceful_shutdown(shutdown_signal());
|
||||
graceful.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -128,20 +121,16 @@ impl InnerService {
|
||||
let uri = req.uri().clone();
|
||||
let cors = self.args.cors;
|
||||
|
||||
let timestamp = Local::now().format("%d/%b/%Y %H:%M:%S");
|
||||
let mut res = match self.handle(req).await {
|
||||
Ok(res) => {
|
||||
println!(r#"[{}] "{} {}" - {}"#, timestamp, method, uri, res.status());
|
||||
info!(r#""{} {}" - {}"#, method, uri, res.status());
|
||||
res
|
||||
}
|
||||
Err(err) => {
|
||||
let mut res = Response::default();
|
||||
let status = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
status!(res, status);
|
||||
eprintln!(
|
||||
r#"[{}] "{} {}" - {} {}"#,
|
||||
timestamp, method, uri, status, err
|
||||
);
|
||||
error!(r#""{} {}" - {} {}"#, method, uri, status, err);
|
||||
res
|
||||
}
|
||||
};
|
||||
@@ -172,11 +161,10 @@ impl InnerService {
|
||||
|
||||
let query = req.uri().query().unwrap_or_default();
|
||||
|
||||
let meta = fs::metadata(path).await.ok();
|
||||
|
||||
let is_miss = meta.is_none();
|
||||
let is_dir = meta.map(|v| v.is_dir()).unwrap_or_default();
|
||||
let is_file = !is_miss && !is_dir;
|
||||
let (is_miss, is_dir, is_file, size) = match fs::metadata(path).await.ok() {
|
||||
Some(meta) => (false, meta.is_dir(), meta.is_file(), meta.len()),
|
||||
None => (true, false, false, 0),
|
||||
};
|
||||
|
||||
let allow_upload = self.args.allow_upload;
|
||||
let allow_delete = self.args.allow_delete;
|
||||
@@ -188,9 +176,10 @@ impl InnerService {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
let headers = req.headers();
|
||||
|
||||
match req.method() {
|
||||
&Method::GET => {
|
||||
let headers = req.headers();
|
||||
if is_dir {
|
||||
if render_index || render_spa {
|
||||
self.handle_render_index(path, headers, &mut res).await?;
|
||||
@@ -215,7 +204,7 @@ impl InnerService {
|
||||
self.handle_method_options(&mut res);
|
||||
}
|
||||
&Method::PUT => {
|
||||
if !allow_upload || (!allow_delete && is_file) {
|
||||
if !allow_upload || (!allow_delete && is_file && size > 0) {
|
||||
status!(res, StatusCode::FORBIDDEN);
|
||||
} else {
|
||||
self.handle_upload(path, req, &mut res).await?;
|
||||
@@ -230,22 +219,52 @@ impl InnerService {
|
||||
status!(res, StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
&Method::HEAD => {
|
||||
if is_miss {
|
||||
status!(res, StatusCode::NOT_FOUND);
|
||||
} else {
|
||||
status!(res, StatusCode::OK);
|
||||
}
|
||||
}
|
||||
method => match method.as_str() {
|
||||
"PROPFIND" => {
|
||||
if is_dir {
|
||||
self.handle_propfind_dir(path, &mut res).await?;
|
||||
self.handle_propfind_dir(path, headers, &mut res).await?;
|
||||
} else if is_file {
|
||||
self.handle_propfind_file(path, &mut res).await?;
|
||||
} else {
|
||||
status!(res, StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
"PROPPATCH" => {
|
||||
if is_file {
|
||||
self.handle_proppatch(req_path, &mut res).await?;
|
||||
} else {
|
||||
status!(res, StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
"MKCOL" if allow_upload && is_miss => self.handle_mkcol(path, &mut res).await?,
|
||||
"COPY" if allow_upload && !is_miss => {
|
||||
self.handle_copy(path, req.headers(), &mut res).await?
|
||||
self.handle_copy(path, headers, &mut res).await?
|
||||
}
|
||||
"MOVE" if allow_upload && allow_delete && !is_miss => {
|
||||
self.handle_move(path, req.headers(), &mut res).await?
|
||||
self.handle_move(path, headers, &mut res).await?
|
||||
}
|
||||
"LOCK" => {
|
||||
// Fake lock
|
||||
if is_file {
|
||||
self.handle_lock(req_path, &mut res).await?;
|
||||
} else {
|
||||
status!(res, StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
"UNLOCK" => {
|
||||
// Fake unlock
|
||||
if is_miss {
|
||||
status!(res, StatusCode::NOT_FOUND);
|
||||
} else {
|
||||
status!(res, StatusCode::OK);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
status!(res, StatusCode::METHOD_NOT_ALLOWED);
|
||||
@@ -292,7 +311,7 @@ impl InnerService {
|
||||
async fn handle_ls_dir(&self, path: &Path, exist: bool, res: &mut Response) -> BoxResult<()> {
|
||||
let mut paths = vec![];
|
||||
if exist {
|
||||
paths = match self.list_dir(path, path, false).await {
|
||||
paths = match self.list_dir(path, path).await {
|
||||
Ok(paths) => paths,
|
||||
Err(_) => {
|
||||
status!(res, StatusCode::FORBIDDEN);
|
||||
@@ -334,18 +353,25 @@ impl InnerService {
|
||||
|
||||
async fn handle_zip_dir(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
|
||||
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
|
||||
let filename = path.file_name().unwrap().to_str().unwrap();
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|v| v.to_str())
|
||||
.ok_or_else(|| format!("Failed to get name of `{}`", path.display()))?;
|
||||
let path = path.to_owned();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = zip_dir(&mut writer, &path).await {
|
||||
eprintln!("Failed to zip {}, {}", path.display(), e);
|
||||
error!("Failed to zip {}, {}", path.display(), e);
|
||||
}
|
||||
});
|
||||
let stream = ReaderStream::new(reader);
|
||||
*res.body_mut() = Body::wrap_stream(stream);
|
||||
res.headers_mut().insert(
|
||||
CONTENT_DISPOSITION,
|
||||
HeaderValue::from_str(&format!("attachment; filename=\"{}.zip\"", filename,)).unwrap(),
|
||||
HeaderValue::from_str(&format!(
|
||||
"attachment; filename=\"{}.zip\"",
|
||||
encode_uri(filename),
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -454,47 +480,57 @@ impl InnerService {
|
||||
}
|
||||
|
||||
fn handle_method_options(&self, res: &mut Response) {
|
||||
let allow_upload = self.args.allow_upload;
|
||||
let allow_delete = self.args.allow_delete;
|
||||
let mut methods = vec!["GET", "PROPFIND", "OPTIONS"];
|
||||
if allow_upload {
|
||||
methods.extend(["PUT", "COPY", "MKCOL"]);
|
||||
}
|
||||
if allow_delete {
|
||||
methods.push("DELETE");
|
||||
}
|
||||
if allow_upload && allow_delete {
|
||||
methods.push("COPY");
|
||||
}
|
||||
let value = methods.join(",").parse().unwrap();
|
||||
res.headers_mut().insert("Allow", value);
|
||||
res.headers_mut().insert(
|
||||
"Allow",
|
||||
"GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
res.headers_mut().insert("DAV", "1".parse().unwrap());
|
||||
|
||||
status!(res, StatusCode::NO_CONTENT);
|
||||
}
|
||||
|
||||
async fn handle_propfind_dir(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
|
||||
let paths = match self.list_dir(path, &self.args.path, true).await {
|
||||
Ok(paths) => paths,
|
||||
Err(_) => {
|
||||
status!(res, StatusCode::FORBIDDEN);
|
||||
return Ok(());
|
||||
}
|
||||
async fn handle_propfind_dir(
|
||||
&self,
|
||||
path: &Path,
|
||||
headers: &HeaderMap<HeaderValue>,
|
||||
res: &mut Response,
|
||||
) -> BoxResult<()> {
|
||||
let depth: u32 = match headers.get("depth") {
|
||||
Some(v) => match v.to_str().ok().and_then(|v| v.parse().ok()) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
status!(res, StatusCode::BAD_REQUEST);
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
let mut paths = vec![self.to_pathitem(path, &self.args.path).await?.unwrap()];
|
||||
if depth > 0 {
|
||||
match self.list_dir(path, &self.args.path).await {
|
||||
Ok(child) => paths.extend(child),
|
||||
Err(_) => {
|
||||
status!(res, StatusCode::FORBIDDEN);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
let output = paths
|
||||
.iter()
|
||||
.map(|v| v.xml(self.args.path_prefix.as_ref()))
|
||||
.map(|v| v.to_dav_xml(self.args.uri_prefix.as_str()))
|
||||
.fold(String::new(), |mut acc, v| {
|
||||
acc.push_str(&v);
|
||||
acc
|
||||
});
|
||||
res_propfind(res, &output);
|
||||
res_multistatus(res, &output);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
|
||||
if let Some(pathitem) = self.to_pathitem(path, &self.args.path).await? {
|
||||
res_propfind(res, &pathitem.xml(self.args.path_prefix.as_ref()));
|
||||
res_multistatus(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str()));
|
||||
} else {
|
||||
status!(res, StatusCode::NOT_FOUND);
|
||||
}
|
||||
@@ -557,6 +593,47 @@ impl InnerService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_lock(&self, req_path: &str, res: &mut Response) -> BoxResult<()> {
|
||||
let token = if self.args.auth.is_none() {
|
||||
Utc::now().timestamp().to_string()
|
||||
} else {
|
||||
format!("opaquelocktoken:{}", Uuid::new_v4())
|
||||
};
|
||||
|
||||
res.headers_mut().insert(
|
||||
"content-type",
|
||||
"application/xml; charset=utf-8".parse().unwrap(),
|
||||
);
|
||||
res.headers_mut()
|
||||
.insert("lock-token", format!("<{}>", token).parse().unwrap());
|
||||
|
||||
*res.body_mut() = Body::from(format!(
|
||||
r#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:prop xmlns:D="DAV:"><D:lockdiscovery><D:activelock>
|
||||
<D:locktoken><D:href>{}</D:href></D:locktoken>
|
||||
<D:lockroot><D:href>{}</D:href></D:lockroot>
|
||||
</D:activelock></D:lockdiscovery></D:prop>"#,
|
||||
token, req_path
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_proppatch(&self, req_path: &str, res: &mut Response) -> BoxResult<()> {
|
||||
let output = format!(
|
||||
r#"<D:response>
|
||||
<D:href>{}</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 403 Forbidden</D:status>
|
||||
</D:propstat>
|
||||
</D:response>"#,
|
||||
req_path
|
||||
);
|
||||
res_multistatus(res, &output);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_index(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -595,29 +672,29 @@ impl InnerService {
|
||||
}
|
||||
|
||||
fn auth_guard(&self, req: &Request, res: &mut Response) -> bool {
|
||||
let method = req.method();
|
||||
let pass = {
|
||||
match &self.args.auth {
|
||||
None => true,
|
||||
Some(auth) => match req.headers().get(AUTHORIZATION) {
|
||||
Some(value) => match value.to_str().ok().map(|v| {
|
||||
let mut it = v.split(' ');
|
||||
(it.next(), it.next())
|
||||
}) {
|
||||
Some((Some("Basic"), Some(tail))) => base64::decode(tail)
|
||||
.ok()
|
||||
.and_then(|v| String::from_utf8(v).ok())
|
||||
.map(|v| v.as_str() == auth)
|
||||
.unwrap_or_default(),
|
||||
_ => false,
|
||||
},
|
||||
None => self.args.no_auth_access && req.method() == Method::GET,
|
||||
Some((user, pass)) => match req.headers().get(AUTHORIZATION) {
|
||||
Some(value) => {
|
||||
valid_digest(value, method.as_str(), user.as_str(), pass.as_str()).is_some()
|
||||
}
|
||||
None => {
|
||||
self.args.no_auth_access
|
||||
&& (method == Method::GET
|
||||
|| method == Method::OPTIONS
|
||||
|| method == Method::HEAD
|
||||
|| method.as_str() == "PROPFIND")
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
if !pass {
|
||||
let value = generate_www_auth(false);
|
||||
status!(res, StatusCode::UNAUTHORIZED);
|
||||
res.headers_mut()
|
||||
.insert(WWW_AUTHENTICATE, HeaderValue::from_static("Basic"));
|
||||
.insert(WWW_AUTHENTICATE, value.parse().unwrap());
|
||||
}
|
||||
pass
|
||||
}
|
||||
@@ -652,25 +729,15 @@ impl InnerService {
|
||||
|
||||
fn strip_path_prefix<'a, P: AsRef<Path>>(&self, path: &'a P) -> Option<&'a Path> {
|
||||
let path = path.as_ref();
|
||||
match self.args.path_prefix.as_deref() {
|
||||
Some(prefix) => {
|
||||
let prefix = prefix.trim_start_matches('/');
|
||||
path.strip_prefix(prefix).ok()
|
||||
}
|
||||
None => Some(path),
|
||||
if self.args.path_prefix.is_empty() {
|
||||
Some(path)
|
||||
} else {
|
||||
path.strip_prefix(&self.args.path_prefix).ok()
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_dir(
|
||||
&self,
|
||||
entry_path: &Path,
|
||||
base_path: &Path,
|
||||
include_entry: bool,
|
||||
) -> BoxResult<Vec<PathItem>> {
|
||||
async fn list_dir(&self, entry_path: &Path, base_path: &Path) -> BoxResult<Vec<PathItem>> {
|
||||
let mut paths: Vec<PathItem> = vec![];
|
||||
if include_entry {
|
||||
paths.push(self.to_pathitem(entry_path, base_path).await?.unwrap())
|
||||
}
|
||||
let mut rd = fs::read_dir(entry_path).await?;
|
||||
while let Ok(Some(entry)) = rd.next_entry().await {
|
||||
let entry_path = entry.path();
|
||||
@@ -740,11 +807,7 @@ struct PathItem {
|
||||
}
|
||||
|
||||
impl PathItem {
|
||||
pub fn xml(&self, prefix: Option<&String>) -> String {
|
||||
let prefix = match prefix {
|
||||
Some(value) => format!("/{}/", value),
|
||||
None => "/".to_owned(),
|
||||
};
|
||||
pub fn to_dav_xml(&self, prefix: &str) -> String {
|
||||
let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822();
|
||||
match self.path_type {
|
||||
PathType::Dir | PathType::SymlinkDir => format!(
|
||||
@@ -755,14 +818,14 @@ impl PathItem {
|
||||
<D:displayname>{}</D:displayname>
|
||||
<D:getlastmodified>{}</D:getlastmodified>
|
||||
<D:resourcetype><D:collection/></D:resourcetype>
|
||||
<D:lockdiscovery/>
|
||||
<D:supportedlock>
|
||||
</D:supportedlock>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>"#,
|
||||
prefix, self.name, self.base_name, mtime
|
||||
escape_str_pcdata(prefix),
|
||||
escape_str_pcdata(&self.name),
|
||||
escape_str_pcdata(&self.base_name),
|
||||
mtime
|
||||
),
|
||||
PathType::File | PathType::SymlinkFile => format!(
|
||||
r#"<D:response>
|
||||
@@ -773,16 +836,13 @@ impl PathItem {
|
||||
<D:getcontentlength>{}</D:getcontentlength>
|
||||
<D:getlastmodified>{}</D:getlastmodified>
|
||||
<D:resourcetype></D:resourcetype>
|
||||
<D:lockdiscovery/>
|
||||
<D:supportedlock>
|
||||
</D:supportedlock>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>"#,
|
||||
prefix,
|
||||
self.name,
|
||||
self.base_name,
|
||||
escape_str_pcdata(prefix),
|
||||
escape_str_pcdata(&self.name),
|
||||
escape_str_pcdata(&self.base_name),
|
||||
self.size.unwrap_or_default(),
|
||||
mtime
|
||||
),
|
||||
@@ -832,8 +892,12 @@ fn add_cors(res: &mut Response) {
|
||||
);
|
||||
}
|
||||
|
||||
fn res_propfind(res: &mut Response, content: &str) {
|
||||
fn res_multistatus(res: &mut Response, content: &str) {
|
||||
*res.status_mut() = StatusCode::MULTI_STATUS;
|
||||
res.headers_mut().insert(
|
||||
"content-type",
|
||||
"application/xml; charset=utf-8".parse().unwrap(),
|
||||
);
|
||||
*res.body_mut() = Body::from(format!(
|
||||
r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:multistatus xmlns:D="DAV:">
|
||||
@@ -910,21 +974,25 @@ fn to_content_range(range: &Range, complete_length: u64) -> Option<ContentRange>
|
||||
})
|
||||
}
|
||||
|
||||
fn print_listening(address: &str, port: u16, tls: bool) {
|
||||
let addrs = retrive_listening_addrs(address);
|
||||
fn print_listening(address: &str, port: u16, prefix: &str, tls: bool) {
|
||||
let prefix = encode_uri(prefix.trim_end_matches('/'));
|
||||
let addrs = retrieve_listening_addrs(address);
|
||||
let protocol = if tls { "https" } else { "http" };
|
||||
if addrs.len() == 1 {
|
||||
eprintln!("Listening on {}://{}:{}", protocol, addrs[0], port);
|
||||
eprintln!(
|
||||
"Listening on {}://{}:{}{}",
|
||||
protocol, addrs[0], port, prefix
|
||||
);
|
||||
} else {
|
||||
eprintln!("Listening on:");
|
||||
for addr in addrs {
|
||||
eprintln!(" {}://{}:{}", protocol, addr, port);
|
||||
eprintln!(" {}://{}:{}{}", protocol, addr, port, prefix);
|
||||
}
|
||||
eprintln!();
|
||||
}
|
||||
}
|
||||
|
||||
fn retrive_listening_addrs(address: &str) -> Vec<String> {
|
||||
fn retrieve_listening_addrs(address: &str) -> Vec<String> {
|
||||
if address == "0.0.0.0" {
|
||||
if let Ok(interfaces) = get_if_addrs() {
|
||||
let mut ifaces: Vec<IpAddr> = interfaces
|
||||
@@ -939,8 +1007,7 @@ fn retrive_listening_addrs(address: &str) -> Vec<String> {
|
||||
vec![address.to_owned()]
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to install CTRL+C signal handler")
|
||||
fn encode_uri(v: &str) -> String {
|
||||
let parts: Vec<_> = v.split('/').map(urlencoding::encode).collect();
|
||||
parts.join("/")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user