feat: support authentication via token (#522)
parent
f8b69f4df8
commit
f26e22e726
|
@ -514,6 +514,7 @@ dependencies = [
|
|||
"futures-util",
|
||||
"glob",
|
||||
"headers",
|
||||
"hex",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
|
@ -527,6 +528,7 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
"port_check",
|
||||
"predicates",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rstest",
|
||||
|
@ -1444,7 +1446,7 @@ checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
|
|||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.2",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.1",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
|
@ -1498,13 +1500,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2251,7 +2252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"rand 0.9.0",
|
||||
"rand 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -53,6 +53,8 @@ http-body-util = "0.1"
|
|||
bytes = "1.5"
|
||||
pin-project-lite = "0.2"
|
||||
sha2 = "0.10.8"
|
||||
rand = "0.9.1"
|
||||
hex = "0.4.3"
|
||||
|
||||
[features]
|
||||
default = ["tls"]
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
* @property {boolean} allow_archive
|
||||
* @property {boolean} auth
|
||||
* @property {string} user
|
||||
* @property {string} token
|
||||
* @property {boolean} dir_exists
|
||||
* @property {string} editable
|
||||
*/
|
||||
|
@ -417,12 +418,13 @@ function renderPathsTableHead() {
|
|||
*/
|
||||
function renderPathsTableBody() {
|
||||
if (DATA.paths && DATA.paths.length > 0) {
|
||||
const token = DATA.token || "";
|
||||
const len = DATA.paths.length;
|
||||
if (len > 0) {
|
||||
$pathsTable.classList.remove("hidden");
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
addPath(DATA.paths[i], i);
|
||||
addPath(DATA.paths[i], i, token);
|
||||
}
|
||||
} else {
|
||||
$emptyFolder.textContent = DIR_EMPTY_NOTE;
|
||||
|
@ -434,8 +436,9 @@ function renderPathsTableBody() {
|
|||
* Add pathitem
|
||||
* @param {PathItem} file
|
||||
* @param {number} index
|
||||
* @param {string} token
|
||||
*/
|
||||
function addPath(file, index) {
|
||||
function addPath(file, index, token) {
|
||||
const encodedName = encodedStr(file.name);
|
||||
let url = newUrl(file.name);
|
||||
let actionDelete = "";
|
||||
|
@ -449,27 +452,27 @@ function addPath(file, index) {
|
|||
if (DATA.allow_archive) {
|
||||
actionDownload = `
|
||||
<div class="action-btn">
|
||||
<a href="${url}?zip" title="Download folder as a .zip file">${ICONS.download}</a>
|
||||
<a href="${url}?token=${token}&zip" title="Download folder as a .zip file">${ICONS.download}</a>
|
||||
</div>`;
|
||||
}
|
||||
} else {
|
||||
actionDownload = `
|
||||
<div class="action-btn" >
|
||||
<a href="${url}" title="Download file" download>${ICONS.download}</a>
|
||||
<a href="${url}?token=${token}" title="Download file" download>${ICONS.download}</a>
|
||||
</div>`;
|
||||
}
|
||||
if (DATA.allow_delete) {
|
||||
if (DATA.allow_upload) {
|
||||
actionMove = `<div onclick="movePath(${index})" class="action-btn" id="moveBtn${index}" title="Move to new path">${ICONS.move}</div>`;
|
||||
if (!isDir) {
|
||||
actionEdit = `<a class="action-btn" title="Edit file" target="_blank" href="${url}?edit">${ICONS.edit}</a>`;
|
||||
actionEdit = `<a class="action-btn" title="Edit file" target="_blank" href="${url}?token=${token}&edit">${ICONS.edit}</a>`;
|
||||
}
|
||||
}
|
||||
actionDelete = `
|
||||
<div onclick="deletePath(${index})" class="action-btn" id="deleteBtn${index}" title="Delete">${ICONS.delete}</div>`;
|
||||
}
|
||||
if (!actionEdit && !isDir) {
|
||||
actionView = `<a class="action-btn" title="View file" target="_blank" href="${url}?view">${ICONS.view}</a>`;
|
||||
actionView = `<a class="action-btn" title="View file" target="_blank" href="${url}?token=${token}&view">${ICONS.view}</a>`;
|
||||
}
|
||||
let actionCell = `
|
||||
<td class="cell-actions">
|
||||
|
@ -488,7 +491,7 @@ function addPath(file, index) {
|
|||
${getPathSvg(file.path_type)}
|
||||
</td>
|
||||
<td class="path cell-name">
|
||||
<a href="${url}" ${isDir ? "" : `target="_blank"`}>${encodedName}</a>
|
||||
<a href="${url}?token=${token}" ${isDir ? "" : `target="_blank"`}>${encodedName}</a>
|
||||
</td>
|
||||
<td class="cell-mtime">${formatMtime(file.mtime)}</td>
|
||||
<td class="cell-size">${sizeDisplay}</td>
|
||||
|
@ -530,7 +533,7 @@ async function setupAuth() {
|
|||
$loginBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await checkAuth();
|
||||
} catch {}
|
||||
} catch { }
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
|
80
src/auth.rs
80
src/auth.rs
|
@ -10,11 +10,13 @@ use md5::Context;
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
const REALM: &str = "DUFS";
|
||||
const DIGEST_AUTH_TIMEOUT: u32 = 604800; // 7 days
|
||||
const TOKEN_LENGTH: usize = 32;
|
||||
|
||||
lazy_static! {
|
||||
static ref NONCESTARTHASH: Context = {
|
||||
|
@ -28,7 +30,8 @@ lazy_static! {
|
|||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AccessControl {
|
||||
use_hashed_password: bool,
|
||||
users: IndexMap<String, (String, AccessPaths)>,
|
||||
users: IndexMap<String, (String, UserToken, AccessPaths)>,
|
||||
tokens: IndexMap<UserToken, String>,
|
||||
anonymous: Option<AccessPaths>,
|
||||
}
|
||||
|
||||
|
@ -37,11 +40,53 @@ impl Default for AccessControl {
|
|||
AccessControl {
|
||||
use_hashed_password: false,
|
||||
users: IndexMap::new(),
|
||||
tokens: IndexMap::new(),
|
||||
anonymous: Some(AccessPaths::new(AccessPerm::ReadWrite)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub struct UserToken {
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl UserToken {
|
||||
pub fn new() -> Self {
|
||||
let mut token_seq = [0; TOKEN_LENGTH];
|
||||
rand::fill(&mut token_seq);
|
||||
|
||||
let ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_nanos();
|
||||
|
||||
// To avoid collisions, we XOR the current timestamp with the last 8 bytes of a randomly token.
|
||||
token_seq[TOKEN_LENGTH - 8..]
|
||||
.iter_mut()
|
||||
.zip(ts.to_be_bytes().iter())
|
||||
.for_each(|(dst, src)| *dst ^= *src);
|
||||
|
||||
Self {
|
||||
token: hex::encode(token_seq),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UserToken {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.token)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for UserToken {
|
||||
fn from(token: &String) -> Self {
|
||||
Self {
|
||||
token: token.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessControl {
|
||||
pub fn new(raw_rules: &[&str]) -> Result<Self> {
|
||||
if raw_rules.is_empty() {
|
||||
|
@ -86,12 +131,20 @@ impl AccessControl {
|
|||
if pass.starts_with("$6$") {
|
||||
use_hashed_password = true;
|
||||
}
|
||||
users.insert(user.to_string(), (pass.to_string(), access_paths));
|
||||
users.insert(
|
||||
user.to_string(),
|
||||
(pass.to_string(), UserToken::new(), access_paths),
|
||||
);
|
||||
}
|
||||
let mut tokens = IndexMap::new();
|
||||
users.iter().for_each(|(user, (_, token, _))| {
|
||||
tokens.insert(token.clone(), user.clone());
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
use_hashed_password,
|
||||
users,
|
||||
tokens,
|
||||
anonymous,
|
||||
})
|
||||
}
|
||||
|
@ -100,19 +153,40 @@ impl AccessControl {
|
|||
!self.users.is_empty()
|
||||
}
|
||||
|
||||
pub fn user_token(&self, user: &str) -> Option<&UserToken> {
|
||||
self.users.get(user).map(|(_, token, _)| token)
|
||||
}
|
||||
|
||||
pub fn guard(
|
||||
&self,
|
||||
path: &str,
|
||||
method: &Method,
|
||||
authorization: Option<&HeaderValue>,
|
||||
query_token: Option<&String>,
|
||||
guard_options: bool,
|
||||
) -> (Option<String>, Option<AccessPaths>) {
|
||||
if self.users.is_empty() {
|
||||
return (None, Some(AccessPaths::new(AccessPerm::ReadWrite)));
|
||||
}
|
||||
|
||||
if let Some(token_str) = query_token {
|
||||
let token: UserToken = token_str.into();
|
||||
if let Some(user) = self.tokens.get(&token) {
|
||||
if let Some((_, _, ap)) = self.users.get(user) {
|
||||
if method == Method::OPTIONS {
|
||||
return (
|
||||
Some(user.clone()),
|
||||
Some(AccessPaths::new(AccessPerm::ReadOnly)),
|
||||
);
|
||||
}
|
||||
return (Some(user.clone()), ap.guard(path, method));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(authorization) = authorization {
|
||||
if let Some(user) = get_auth_user(authorization) {
|
||||
if let Some((pass, ap)) = self.users.get(&user) {
|
||||
if let Some((pass, _, ap)) = self.users.get(&user) {
|
||||
if method == Method::OPTIONS {
|
||||
return (Some(user), Some(AccessPaths::new(AccessPerm::ReadOnly)));
|
||||
}
|
||||
|
|
|
@ -163,6 +163,13 @@ impl Server {
|
|||
let headers = req.headers();
|
||||
let method = req.method().clone();
|
||||
|
||||
let query = req.uri().query().unwrap_or_default();
|
||||
let query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
let query_token = query_params.get("token");
|
||||
|
||||
let relative_path = match self.resolve_path(req_path) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
|
@ -180,10 +187,14 @@ impl Server {
|
|||
}
|
||||
|
||||
let authorization = headers.get(AUTHORIZATION);
|
||||
let guard =
|
||||
self.args
|
||||
.auth
|
||||
.guard(&relative_path, &method, authorization, is_microsoft_webdav);
|
||||
|
||||
let guard = self.args.auth.guard(
|
||||
&relative_path,
|
||||
&method,
|
||||
authorization,
|
||||
query_token,
|
||||
is_microsoft_webdav,
|
||||
);
|
||||
|
||||
let (user, access_paths) = match guard {
|
||||
(None, None) => {
|
||||
|
@ -197,11 +208,6 @@ impl Server {
|
|||
(x, Some(y)) => (x, y),
|
||||
};
|
||||
|
||||
let query = req.uri().query().unwrap_or_default();
|
||||
let query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
if method.as_str() == "CHECKAUTH" {
|
||||
*res.body_mut() = body_full(user.clone().unwrap_or_default());
|
||||
return Ok(res);
|
||||
|
@ -1186,6 +1192,12 @@ impl Server {
|
|||
normalize_path(path.strip_prefix(&self.args.serve_path)?)
|
||||
);
|
||||
let readwrite = access_paths.perm().readwrite();
|
||||
|
||||
let token = match &user {
|
||||
Some(user) => self.args.auth.user_token(user).map(|v| v.to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let data = IndexData {
|
||||
kind: DataKind::Index,
|
||||
href,
|
||||
|
@ -1197,6 +1209,7 @@ impl Server {
|
|||
dir_exists: exist,
|
||||
auth: self.args.auth.exist(),
|
||||
user,
|
||||
token,
|
||||
paths,
|
||||
};
|
||||
let output = if has_query_flag(query_params, "json") {
|
||||
|
@ -1259,11 +1272,18 @@ impl Server {
|
|||
}
|
||||
};
|
||||
|
||||
let query = req.uri().query().unwrap_or_default();
|
||||
let query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
let query_token = query_params.get("token");
|
||||
|
||||
let authorization = headers.get(AUTHORIZATION);
|
||||
let guard = self
|
||||
.args
|
||||
.auth
|
||||
.guard(&dest_path, req.method(), authorization, false);
|
||||
let guard =
|
||||
self.args
|
||||
.auth
|
||||
.guard(&dest_path, req.method(), authorization, query_token, false);
|
||||
|
||||
match guard {
|
||||
(_, Some(_)) => {}
|
||||
|
@ -1435,6 +1455,7 @@ struct IndexData {
|
|||
dir_exists: bool,
|
||||
auth: bool,
|
||||
user: Option<String>,
|
||||
token: Option<String>,
|
||||
paths: Vec<PathItem>,
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue