feat: path level access control (#52)
BREAKING CHANGE: `--auth` is changed, `--no-auth-access` is removed
This commit is contained in:
parent
9384cc8587
commit
9c2e9d1503
7 changed files with 288 additions and 94 deletions
49
README.md
49
README.md
|
@ -14,7 +14,7 @@ Duf is a simple file server. Support static serve, search, upload, webdav...
|
||||||
- Upload files and folders (Drag & Drop)
|
- Upload files and folders (Drag & Drop)
|
||||||
- Search files
|
- Search files
|
||||||
- Partial responses (Parallel/Resume download)
|
- Partial responses (Parallel/Resume download)
|
||||||
- Authentication
|
- Path level access control
|
||||||
- Support https
|
- Support https
|
||||||
- Support webdav
|
- Support webdav
|
||||||
- Easy to use with curl
|
- Easy to use with curl
|
||||||
|
@ -88,12 +88,6 @@ Listen on a specific port
|
||||||
duf -p 80
|
duf -p 80
|
||||||
```
|
```
|
||||||
|
|
||||||
Protect with authentication
|
|
||||||
|
|
||||||
```
|
|
||||||
duf -a admin:admin
|
|
||||||
```
|
|
||||||
|
|
||||||
For a single page application (SPA)
|
For a single page application (SPA)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -110,27 +104,60 @@ duf --tls-cert my.crt --tls-key my.key
|
||||||
|
|
||||||
Download a file
|
Download a file
|
||||||
```
|
```
|
||||||
curl http://127.0.0.1:5000/some-file
|
curl http://127.0.0.1:5000/path-to-file
|
||||||
```
|
```
|
||||||
|
|
||||||
Download a folder as zip file
|
Download a folder as zip file
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -o some-folder.zip http://127.0.0.1:5000/some-folder?zip
|
curl -o path-to-folder.zip http://127.0.0.1:5000/path-to-folder?zip
|
||||||
```
|
```
|
||||||
|
|
||||||
Upload a file
|
Upload a file
|
||||||
|
|
||||||
```
|
```
|
||||||
curl --upload-file some-file http://127.0.0.1:5000/some-file
|
curl --upload-file path-to-file http://127.0.0.1:5000/path-to-file
|
||||||
```
|
```
|
||||||
|
|
||||||
Delete a file/folder
|
Delete a file/folder
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -X DELETE http://127.0.0.1:5000/some-file
|
curl -X DELETE http://127.0.0.1:5000/path-to-file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>Duf supports path level access control with --auth/-a option.</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
duf -a <path>@<readwrite>[@<readonly>]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `<path>`: Path to protected
|
||||||
|
- `<readwrite>`: Account with readwrite permission, required
|
||||||
|
- `<readonly>`: Account with readonly permission, optional
|
||||||
|
|
||||||
|
> `*` as `<readonly>` means `<path>` is public, everyone can access/download it.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
duf -a /@admin:pass@* -a /ui@designer:pass1 -A
|
||||||
|
```
|
||||||
|
- All files/folders are public to access/download.
|
||||||
|
- Account `admin:pass` can upload/delete/download any files/folders.
|
||||||
|
- Account `designer:pass1` can upload/delete/download any files/folders in the `ui` folder.
|
||||||
|
|
||||||
|
Curl with auth:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl --digest -u designer:pass1 http://127.0.0.1:5000/ui/path-to-file
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2022 duf-developers.
|
Copyright (c) 2022 duf-developers.
|
||||||
|
|
29
src/args.rs
29
src/args.rs
|
@ -4,7 +4,7 @@ use std::env;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::auth::parse_auth;
|
use crate::auth::AccessControl;
|
||||||
use crate::tls::{load_certs, load_private_key};
|
use crate::tls::{load_certs, load_private_key};
|
||||||
use crate::BoxResult;
|
use crate::BoxResult;
|
||||||
|
|
||||||
|
@ -51,13 +51,10 @@ fn app() -> Command<'static> {
|
||||||
Arg::new("auth")
|
Arg::new("auth")
|
||||||
.short('a')
|
.short('a')
|
||||||
.long("auth")
|
.long("auth")
|
||||||
.help("Use HTTP authentication")
|
.help("Add auth for path")
|
||||||
.value_name("user:pass"),
|
.multiple_values(true)
|
||||||
)
|
.multiple_occurrences(true)
|
||||||
.arg(
|
.value_name("rule"),
|
||||||
Arg::new("no-auth-access")
|
|
||||||
.long("no-auth-access")
|
|
||||||
.help("Not required auth when access static files"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("allow-all")
|
Arg::new("allow-all")
|
||||||
|
@ -118,15 +115,14 @@ pub fn matches() -> ArgMatches {
|
||||||
app().get_matches()
|
app().get_matches()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
pub addrs: Vec<IpAddr>,
|
pub addrs: Vec<IpAddr>,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub path_prefix: String,
|
pub path_prefix: String,
|
||||||
pub uri_prefix: String,
|
pub uri_prefix: String,
|
||||||
pub auth: Option<(String, String)>,
|
pub auth: AccessControl,
|
||||||
pub no_auth_access: bool,
|
|
||||||
pub allow_upload: bool,
|
pub allow_upload: bool,
|
||||||
pub allow_delete: bool,
|
pub allow_delete: bool,
|
||||||
pub allow_symlink: bool,
|
pub allow_symlink: bool,
|
||||||
|
@ -160,11 +156,11 @@ impl Args {
|
||||||
format!("/{}/", &path_prefix)
|
format!("/{}/", &path_prefix)
|
||||||
};
|
};
|
||||||
let cors = matches.is_present("cors");
|
let cors = matches.is_present("cors");
|
||||||
let auth = match matches.value_of("auth") {
|
let auth: Vec<&str> = matches
|
||||||
Some(auth) => Some(parse_auth(auth)?),
|
.values_of("auth")
|
||||||
None => None,
|
.map(|v| v.collect())
|
||||||
};
|
.unwrap_or_default();
|
||||||
let no_auth_access = matches.is_present("no-auth-access");
|
let auth = AccessControl::new(&auth, &uri_prefix)?;
|
||||||
let allow_upload = matches.is_present("allow-all") || matches.is_present("allow-upload");
|
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");
|
let allow_delete = matches.is_present("allow-all") || matches.is_present("allow-delete");
|
||||||
let allow_symlink = matches.is_present("allow-all") || matches.is_present("allow-symlink");
|
let allow_symlink = matches.is_present("allow-all") || matches.is_present("allow-symlink");
|
||||||
|
@ -187,7 +183,6 @@ impl Args {
|
||||||
path_prefix,
|
path_prefix,
|
||||||
uri_prefix,
|
uri_prefix,
|
||||||
auth,
|
auth,
|
||||||
no_auth_access,
|
|
||||||
cors,
|
cors,
|
||||||
allow_delete,
|
allow_delete,
|
||||||
allow_upload,
|
allow_upload,
|
||||||
|
|
164
src/auth.rs
164
src/auth.rs
|
@ -1,4 +1,5 @@
|
||||||
use headers::HeaderValue;
|
use headers::HeaderValue;
|
||||||
|
use hyper::Method;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use md5::Context;
|
use md5::Context;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -7,6 +8,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::utils::encode_uri;
|
||||||
use crate::BoxResult;
|
use crate::BoxResult;
|
||||||
|
|
||||||
const REALM: &str = "DUF";
|
const REALM: &str = "DUF";
|
||||||
|
@ -20,6 +22,151 @@ lazy_static! {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AccessControl {
|
||||||
|
rules: HashMap<String, PathControl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PathControl {
|
||||||
|
readwrite: Account,
|
||||||
|
readonly: Option<Account>,
|
||||||
|
share: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccessControl {
|
||||||
|
pub fn new(raw_rules: &[&str], uri_prefix: &str) -> BoxResult<Self> {
|
||||||
|
let mut rules = HashMap::default();
|
||||||
|
if raw_rules.is_empty() {
|
||||||
|
return Ok(Self { rules });
|
||||||
|
}
|
||||||
|
for rule in raw_rules {
|
||||||
|
let parts: Vec<&str> = rule.split('@').collect();
|
||||||
|
let create_err = || format!("Invalid auth `{}`", rule).into();
|
||||||
|
match parts.as_slice() {
|
||||||
|
[path, readwrite] => {
|
||||||
|
let control = PathControl {
|
||||||
|
readwrite: Account::new(readwrite).ok_or_else(create_err)?,
|
||||||
|
readonly: None,
|
||||||
|
share: false,
|
||||||
|
};
|
||||||
|
rules.insert(sanitize_path(path, uri_prefix), control);
|
||||||
|
}
|
||||||
|
[path, readwrite, readonly] => {
|
||||||
|
let (readonly, share) = if *readonly == "*" {
|
||||||
|
(None, true)
|
||||||
|
} else {
|
||||||
|
(Some(Account::new(readonly).ok_or_else(create_err)?), false)
|
||||||
|
};
|
||||||
|
let control = PathControl {
|
||||||
|
readwrite: Account::new(readwrite).ok_or_else(create_err)?,
|
||||||
|
readonly,
|
||||||
|
share,
|
||||||
|
};
|
||||||
|
rules.insert(sanitize_path(path, uri_prefix), control);
|
||||||
|
}
|
||||||
|
_ => return Err(create_err()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self { rules })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guard(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
method: &Method,
|
||||||
|
authorization: Option<&HeaderValue>,
|
||||||
|
) -> GuardType {
|
||||||
|
if self.rules.is_empty() {
|
||||||
|
return GuardType::ReadWrite;
|
||||||
|
}
|
||||||
|
let mut controls = vec![];
|
||||||
|
for path in walk_path(path) {
|
||||||
|
if let Some(control) = self.rules.get(path) {
|
||||||
|
controls.push(control);
|
||||||
|
if let Some(authorization) = authorization {
|
||||||
|
let Account { user, pass } = &control.readwrite;
|
||||||
|
if valid_digest(authorization, method.as_str(), user, pass).is_some() {
|
||||||
|
return GuardType::ReadWrite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_readonly_method(method) {
|
||||||
|
for control in controls.into_iter() {
|
||||||
|
if control.share {
|
||||||
|
return GuardType::ReadOnly;
|
||||||
|
}
|
||||||
|
if let Some(authorization) = authorization {
|
||||||
|
if let Some(Account { user, pass }) = &control.readonly {
|
||||||
|
if valid_digest(authorization, method.as_str(), user, pass).is_some() {
|
||||||
|
return GuardType::ReadOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GuardType::Reject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum GuardType {
|
||||||
|
Reject,
|
||||||
|
ReadWrite,
|
||||||
|
ReadOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuardType {
|
||||||
|
pub fn is_reject(&self) -> bool {
|
||||||
|
*self == GuardType::Reject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_path(path: &str, uri_prefix: &str) -> String {
|
||||||
|
encode_uri(&format!("{}{}", uri_prefix, path.trim_matches('/')))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_path(path: &str) -> impl Iterator<Item = &str> {
|
||||||
|
let mut idx = 0;
|
||||||
|
path.split('/').enumerate().map(move |(i, part)| {
|
||||||
|
let end = if i == 0 { 1 } else { idx + part.len() + i };
|
||||||
|
let value = &path[..end];
|
||||||
|
idx += part.len();
|
||||||
|
value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_readonly_method(method: &Method) -> bool {
|
||||||
|
method == Method::GET
|
||||||
|
|| method == Method::OPTIONS
|
||||||
|
|| method == Method::HEAD
|
||||||
|
|| method.as_str() == "PROPFIND"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Account {
|
||||||
|
user: String,
|
||||||
|
pass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Account {
|
||||||
|
fn new(data: &str) -> Option<Self> {
|
||||||
|
let p: Vec<&str> = data.trim().split(':').collect();
|
||||||
|
if p.len() != 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let user = p[0];
|
||||||
|
let pass = p[1];
|
||||||
|
let mut h = Context::new();
|
||||||
|
h.consume(format!("{}:{}:{}", user, REALM, pass).as_bytes());
|
||||||
|
Some(Account {
|
||||||
|
user: user.to_owned(),
|
||||||
|
pass: format!("{:x}", h.compute()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_www_auth(stale: bool) -> String {
|
pub fn generate_www_auth(stale: bool) -> String {
|
||||||
let str_stale = if stale { "stale=true," } else { "" };
|
let str_stale = if stale { "stale=true," } else { "" };
|
||||||
format!(
|
format!(
|
||||||
|
@ -30,26 +177,13 @@ pub fn generate_www_auth(stale: bool) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
pub fn valid_digest(
|
||||||
header_value: &HeaderValue,
|
authorization: &HeaderValue,
|
||||||
method: &str,
|
method: &str,
|
||||||
auth_user: &str,
|
auth_user: &str,
|
||||||
auth_pass: &str,
|
auth_pass: &str,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let digest_value = strip_prefix(header_value.as_bytes(), b"Digest ")?;
|
let digest_value = strip_prefix(authorization.as_bytes(), b"Digest ")?;
|
||||||
let user_vals = to_headermap(digest_value).ok()?;
|
let user_vals = to_headermap(digest_value).ok()?;
|
||||||
if let (Some(username), Some(nonce), Some(user_response)) = (
|
if let (Some(username), Some(nonce), Some(user_response)) = (
|
||||||
user_vals
|
user_vals
|
||||||
|
|
|
@ -3,6 +3,7 @@ mod auth;
|
||||||
mod server;
|
mod server;
|
||||||
mod streamer;
|
mod streamer;
|
||||||
mod tls;
|
mod tls;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::auth::{generate_www_auth, valid_digest};
|
use crate::auth::generate_www_auth;
|
||||||
use crate::streamer::Streamer;
|
use crate::streamer::Streamer;
|
||||||
|
use crate::utils::{decode_uri, encode_uri};
|
||||||
use crate::{Args, BoxResult};
|
use crate::{Args, BoxResult};
|
||||||
use xml::escape::escape_str_pcdata;
|
use xml::escape::escape_str_pcdata;
|
||||||
|
|
||||||
|
@ -19,7 +20,6 @@ use hyper::header::{
|
||||||
CONTENT_TYPE, ORIGIN, RANGE, WWW_AUTHENTICATE,
|
CONTENT_TYPE, ORIGIN, RANGE, WWW_AUTHENTICATE,
|
||||||
};
|
};
|
||||||
use hyper::{Body, Method, StatusCode, Uri};
|
use hyper::{Body, Method, StatusCode, Uri};
|
||||||
use percent_encoding::percent_decode;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fs::Metadata;
|
use std::fs::Metadata;
|
||||||
use std::io::SeekFrom;
|
use std::io::SeekFrom;
|
||||||
|
@ -86,16 +86,20 @@ impl Server {
|
||||||
pub async fn handle(self: Arc<Self>, req: Request) -> BoxResult<Response> {
|
pub async fn handle(self: Arc<Self>, req: Request) -> BoxResult<Response> {
|
||||||
let mut res = Response::default();
|
let mut res = Response::default();
|
||||||
|
|
||||||
if !self.auth_guard(&req, &mut res) {
|
|
||||||
return Ok(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
let req_path = req.uri().path();
|
let req_path = req.uri().path();
|
||||||
let headers = req.headers();
|
let headers = req.headers();
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
|
|
||||||
|
let authorization = headers.get(AUTHORIZATION);
|
||||||
|
let guard_type = self.args.auth.guard(req_path, &method, authorization);
|
||||||
|
|
||||||
if req_path == "/favicon.ico" && method == Method::GET {
|
if req_path == "/favicon.ico" && method == Method::GET {
|
||||||
self.handle_send_favicon(req.headers(), &mut res).await?;
|
self.handle_send_favicon(headers, &mut res).await?;
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if guard_type.is_reject() {
|
||||||
|
self.auth_reject(&mut res);
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +110,7 @@ impl Server {
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = path.as_path();
|
let path = path.as_path();
|
||||||
|
|
||||||
let query = req.uri().query().unwrap_or_default();
|
let query = req.uri().query().unwrap_or_default();
|
||||||
|
@ -218,7 +223,8 @@ impl Server {
|
||||||
"LOCK" => {
|
"LOCK" => {
|
||||||
// Fake lock
|
// Fake lock
|
||||||
if is_file {
|
if is_file {
|
||||||
self.handle_lock(req_path, &mut res).await?;
|
let has_auth = authorization.is_some();
|
||||||
|
self.handle_lock(req_path, has_auth, &mut res).await?;
|
||||||
} else {
|
} else {
|
||||||
status_not_found(&mut res);
|
status_not_found(&mut res);
|
||||||
}
|
}
|
||||||
|
@ -618,11 +624,11 @@ impl Server {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_lock(&self, req_path: &str, res: &mut Response) -> BoxResult<()> {
|
async fn handle_lock(&self, req_path: &str, auth: bool, res: &mut Response) -> BoxResult<()> {
|
||||||
let token = if self.args.auth.is_none() {
|
let token = if auth {
|
||||||
Utc::now().timestamp().to_string()
|
|
||||||
} else {
|
|
||||||
format!("opaquelocktoken:{}", Uuid::new_v4())
|
format!("opaquelocktoken:{}", Uuid::new_v4())
|
||||||
|
} else {
|
||||||
|
Utc::now().timestamp().to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
|
@ -708,34 +714,13 @@ const DATA =
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth_guard(&self, req: &Request, res: &mut Response) -> bool {
|
fn auth_reject(&self, res: &mut Response) {
|
||||||
let method = req.method();
|
let value = generate_www_auth(false);
|
||||||
let pass = {
|
set_webdav_headers(res);
|
||||||
match &self.args.auth {
|
res.headers_mut().typed_insert(Connection::close());
|
||||||
None => true,
|
res.headers_mut()
|
||||||
Some((user, pass)) => match req.headers().get(AUTHORIZATION) {
|
.insert(WWW_AUTHENTICATE, value.parse().unwrap());
|
||||||
Some(value) => {
|
*res.status_mut() = StatusCode::UNAUTHORIZED;
|
||||||
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);
|
|
||||||
set_webdav_headers(res);
|
|
||||||
*res.status_mut() = StatusCode::UNAUTHORIZED;
|
|
||||||
res.headers_mut().typed_insert(Connection::close());
|
|
||||||
res.headers_mut()
|
|
||||||
.insert(WWW_AUTHENTICATE, value.parse().unwrap());
|
|
||||||
}
|
|
||||||
pass
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_root_contained(&self, path: &Path) -> bool {
|
async fn is_root_contained(&self, path: &Path) -> bool {
|
||||||
|
@ -753,7 +738,7 @@ const DATA =
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_path(&self, path: &str) -> Option<PathBuf> {
|
fn extract_path(&self, path: &str) -> Option<PathBuf> {
|
||||||
let decoded_path = percent_decode(path[1..].as_bytes()).decode_utf8().ok()?;
|
let decoded_path = decode_uri(&path[1..])?;
|
||||||
let slashes_switched = if cfg!(windows) {
|
let slashes_switched = if cfg!(windows) {
|
||||||
decoded_path.replace('/', "\\")
|
decoded_path.replace('/', "\\")
|
||||||
} else {
|
} else {
|
||||||
|
@ -1023,13 +1008,9 @@ fn parse_range(headers: &HeaderMap<HeaderValue>) -> Option<RangeValue> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_uri(v: &str) -> String {
|
|
||||||
let parts: Vec<_> = v.split('/').map(urlencoding::encode).collect();
|
|
||||||
parts.join("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn status_forbid(res: &mut Response) {
|
fn status_forbid(res: &mut Response) {
|
||||||
*res.status_mut() = StatusCode::FORBIDDEN;
|
*res.status_mut() = StatusCode::FORBIDDEN;
|
||||||
|
*res.body_mut() = Body::from("Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status_not_found(res: &mut Response) {
|
fn status_not_found(res: &mut Response) {
|
||||||
|
|
12
src/utils.rs
Normal file
12
src/utils.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub fn encode_uri(v: &str) -> String {
|
||||||
|
let parts: Vec<_> = v.split('/').map(urlencoding::encode).collect();
|
||||||
|
parts.join("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_uri(v: &str) -> Option<Cow<str>> {
|
||||||
|
percent_encoding::percent_decode(v.as_bytes())
|
||||||
|
.decode_utf8()
|
||||||
|
.ok()
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ use fixtures::{server, Error, TestServer};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn no_auth(#[with(&["--auth", "user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
|
fn no_auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
|
||||||
let resp = reqwest::blocking::get(server.url())?;
|
let resp = reqwest::blocking::get(server.url())?;
|
||||||
assert_eq!(resp.status(), 401);
|
assert_eq!(resp.status(), 401);
|
||||||
assert!(resp.headers().contains_key("www-authenticate"));
|
assert!(resp.headers().contains_key("www-authenticate"));
|
||||||
|
@ -17,7 +17,7 @@ fn no_auth(#[with(&["--auth", "user:pass", "-A"])] server: TestServer) -> Result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn auth(#[with(&["--auth", "user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
|
fn auth(#[with(&["--auth", "/@user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
|
||||||
let url = format!("{}file1", server.url());
|
let url = format!("{}file1", server.url());
|
||||||
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
|
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
|
||||||
assert_eq!(resp.status(), 401);
|
assert_eq!(resp.status(), 401);
|
||||||
|
@ -29,10 +29,54 @@ fn auth(#[with(&["--auth", "user:pass", "-A"])] server: TestServer) -> Result<()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn auth_skip_access(
|
fn auth_skip(#[with(&["--auth", "/@user:pass@*"])] server: TestServer) -> Result<(), Error> {
|
||||||
#[with(&["--auth", "user:pass", "--no-auth-access"])] server: TestServer,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let resp = reqwest::blocking::get(server.url())?;
|
let resp = reqwest::blocking::get(server.url())?;
|
||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn auth_readonly(
|
||||||
|
#[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let url = format!("{}index.html", server.url());
|
||||||
|
let resp = fetch!(b"GET", &url).send()?;
|
||||||
|
assert_eq!(resp.status(), 401);
|
||||||
|
let resp = fetch!(b"GET", &url).send_with_digest_auth("user2", "pass2")?;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
let url = format!("{}file1", server.url());
|
||||||
|
let resp = fetch!(b"PUT", &url)
|
||||||
|
.body(b"abc".to_vec())
|
||||||
|
.send_with_digest_auth("user2", "pass2")?;
|
||||||
|
assert_eq!(resp.status(), 401);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn auth_nest(
|
||||||
|
#[with(&["--auth", "/@user:pass@user2:pass2", "--auth", "/dira@user3:pass3", "-A"])]
|
||||||
|
server: TestServer,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let url = format!("{}dira/file1", server.url());
|
||||||
|
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
|
||||||
|
assert_eq!(resp.status(), 401);
|
||||||
|
let resp = fetch!(b"PUT", &url)
|
||||||
|
.body(b"abc".to_vec())
|
||||||
|
.send_with_digest_auth("user3", "pass3")?;
|
||||||
|
assert_eq!(resp.status(), 201);
|
||||||
|
let resp = fetch!(b"PUT", &url)
|
||||||
|
.body(b"abc".to_vec())
|
||||||
|
.send_with_digest_auth("user", "pass")?;
|
||||||
|
assert_eq!(resp.status(), 201);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn auth_nest_share(
|
||||||
|
#[with(&["--auth", "/@user:pass@*", "--auth", "/dira@user3:pass3", "-A"])] server: TestServer,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let url = format!("{}index.html", server.url());
|
||||||
|
let resp = fetch!(b"GET", &url).send()?;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue