diff --git a/Cargo.lock b/Cargo.lock
index 81a6319..02fe92d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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]]
diff --git a/Cargo.toml b/Cargo.toml
index 51a1e1d..4ea8a18 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"]
diff --git a/assets/index.js b/assets/index.js
index b87e354..66feae1 100644
--- a/assets/index.js
+++ b/assets/index.js
@@ -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 = `
`;
}
} else {
actionDownload = `
`;
}
if (DATA.allow_delete) {
if (DATA.allow_upload) {
actionMove = `${ICONS.move}
`;
if (!isDir) {
- actionEdit = `${ICONS.edit}`;
+ actionEdit = `${ICONS.edit}`;
}
}
actionDelete = `
${ICONS.delete}
`;
}
if (!actionEdit && !isDir) {
- actionView = `${ICONS.view}`;
+ actionView = `${ICONS.view}`;
}
let actionCell = `
@@ -488,7 +491,7 @@ function addPath(file, index) {
${getPathSvg(file.path_type)}
|
- ${encodedName}
+ ${encodedName}
|
${formatMtime(file.mtime)} |
${sizeDisplay} |
@@ -530,7 +533,7 @@ async function setupAuth() {
$loginBtn.addEventListener("click", async () => {
try {
await checkAuth();
- } catch {}
+ } catch { }
location.reload();
});
}
diff --git a/src/auth.rs b/src/auth.rs
index fae91bf..9da8ac6 100644
--- a/src/auth.rs
+++ b/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,
+ users: IndexMap,
+ tokens: IndexMap,
anonymous: Option,
}
@@ -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 {
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, Option) {
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)));
}
diff --git a/src/server.rs b/src/server.rs
index e78f0d5..cfcac8c 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -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 = 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 = 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 = 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,
+ token: Option,
paths: Vec,
}