feat: path level access control (#52)

BREAKING CHANGE: `--auth` is changed, `--no-auth-access` is removed
This commit is contained in:
sigoden
2022-06-19 11:26:03 +08:00
committed by GitHub
parent 9384cc8587
commit 9c2e9d1503
7 changed files with 288 additions and 94 deletions

View File

@@ -4,7 +4,7 @@ use std::env;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use crate::auth::parse_auth;
use crate::auth::AccessControl;
use crate::tls::{load_certs, load_private_key};
use crate::BoxResult;
@@ -51,13 +51,10 @@ fn app() -> Command<'static> {
Arg::new("auth")
.short('a')
.long("auth")
.help("Use HTTP authentication")
.value_name("user:pass"),
)
.arg(
Arg::new("no-auth-access")
.long("no-auth-access")
.help("Not required auth when access static files"),
.help("Add auth for path")
.multiple_values(true)
.multiple_occurrences(true)
.value_name("rule"),
)
.arg(
Arg::new("allow-all")
@@ -118,15 +115,14 @@ pub fn matches() -> ArgMatches {
app().get_matches()
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone)]
pub struct Args {
pub addrs: Vec<IpAddr>,
pub port: u16,
pub path: PathBuf,
pub path_prefix: String,
pub uri_prefix: String,
pub auth: Option<(String, String)>,
pub no_auth_access: bool,
pub auth: AccessControl,
pub allow_upload: bool,
pub allow_delete: bool,
pub allow_symlink: bool,
@@ -160,11 +156,11 @@ impl Args {
format!("/{}/", &path_prefix)
};
let cors = matches.is_present("cors");
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 auth: Vec<&str> = matches
.values_of("auth")
.map(|v| v.collect())
.unwrap_or_default();
let auth = AccessControl::new(&auth, &uri_prefix)?;
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_symlink = matches.is_present("allow-all") || matches.is_present("allow-symlink");
@@ -187,7 +183,6 @@ impl Args {
path_prefix,
uri_prefix,
auth,
no_auth_access,
cors,
allow_delete,
allow_upload,

View File

@@ -1,4 +1,5 @@
use headers::HeaderValue;
use hyper::Method;
use lazy_static::lazy_static;
use md5::Context;
use std::{
@@ -7,6 +8,7 @@ use std::{
};
use uuid::Uuid;
use crate::utils::encode_uri;
use crate::BoxResult;
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 {
let str_stale = if stale { "stale=true," } else { "" };
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(
header_value: &HeaderValue,
authorization: &HeaderValue,
method: &str,
auth_user: &str,
auth_pass: &str,
) -> 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()?;
if let (Some(username), Some(nonce), Some(user_response)) = (
user_vals

View File

@@ -3,6 +3,7 @@ mod auth;
mod server;
mod streamer;
mod tls;
mod utils;
#[macro_use]
extern crate log;

View File

@@ -1,5 +1,6 @@
use crate::auth::{generate_www_auth, valid_digest};
use crate::auth::generate_www_auth;
use crate::streamer::Streamer;
use crate::utils::{decode_uri, encode_uri};
use crate::{Args, BoxResult};
use xml::escape::escape_str_pcdata;
@@ -19,7 +20,6 @@ use hyper::header::{
CONTENT_TYPE, ORIGIN, RANGE, WWW_AUTHENTICATE,
};
use hyper::{Body, Method, StatusCode, Uri};
use percent_encoding::percent_decode;
use serde::Serialize;
use std::fs::Metadata;
use std::io::SeekFrom;
@@ -86,16 +86,20 @@ impl Server {
pub async fn handle(self: Arc<Self>, req: Request) -> BoxResult<Response> {
let mut res = Response::default();
if !self.auth_guard(&req, &mut res) {
return Ok(res);
}
let req_path = req.uri().path();
let headers = req.headers();
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 {
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);
}
@@ -106,6 +110,7 @@ impl Server {
return Ok(res);
}
};
let path = path.as_path();
let query = req.uri().query().unwrap_or_default();
@@ -218,7 +223,8 @@ impl Server {
"LOCK" => {
// Fake lock
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 {
status_not_found(&mut res);
}
@@ -618,11 +624,11 @@ impl Server {
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 {
async fn handle_lock(&self, req_path: &str, auth: bool, res: &mut Response) -> BoxResult<()> {
let token = if auth {
format!("opaquelocktoken:{}", Uuid::new_v4())
} else {
Utc::now().timestamp().to_string()
};
res.headers_mut().insert(
@@ -708,34 +714,13 @@ const DATA =
Ok(())
}
fn auth_guard(&self, req: &Request, res: &mut Response) -> bool {
let method = req.method();
let pass = {
match &self.args.auth {
None => true,
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);
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
fn auth_reject(&self, res: &mut Response) {
let value = generate_www_auth(false);
set_webdav_headers(res);
res.headers_mut().typed_insert(Connection::close());
res.headers_mut()
.insert(WWW_AUTHENTICATE, value.parse().unwrap());
*res.status_mut() = StatusCode::UNAUTHORIZED;
}
async fn is_root_contained(&self, path: &Path) -> bool {
@@ -753,7 +738,7 @@ const DATA =
}
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) {
decoded_path.replace('/', "\\")
} 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) {
*res.status_mut() = StatusCode::FORBIDDEN;
*res.body_mut() = Body::from("Forbidden");
}
fn status_not_found(res: &mut Response) {

12
src/utils.rs Normal file
View 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()
}