feat: support downloading via token auth (#603)
parent
089d30c5a5
commit
9c9fca75d3
|
@ -378,6 +378,12 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "content_inspector"
|
name = "content_inspector"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
@ -446,6 +452,43 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "curve25519-dalek"
|
||||||
|
version = "4.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"curve25519-dalek-derive",
|
||||||
|
"digest",
|
||||||
|
"fiat-crypto",
|
||||||
|
"rustc_version",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "curve25519-dalek-derive"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "difflib"
|
name = "difflib"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -510,10 +553,12 @@ dependencies = [
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"content_inspector",
|
"content_inspector",
|
||||||
"digest_auth",
|
"digest_auth",
|
||||||
|
"ed25519-dalek",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"glob",
|
"glob",
|
||||||
"headers",
|
"headers",
|
||||||
|
"hex",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
|
@ -549,6 +594,30 @@ dependencies = [
|
||||||
"xml-rs",
|
"xml-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||||
|
dependencies = [
|
||||||
|
"pkcs8",
|
||||||
|
"signature",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519-dalek"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"ed25519",
|
||||||
|
"serde",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
@ -580,6 +649,12 @@ version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fiat-crypto"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
@ -1347,6 +1422,16 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs8"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
@ -1879,6 +1964,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signature"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
@ -1915,6 +2009,16 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
|
@ -53,6 +53,8 @@ http-body-util = "0.1"
|
||||||
bytes = "1.5"
|
bytes = "1.5"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
|
ed25519-dalek = "2.2.0"
|
||||||
|
hex = "0.4.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tls"]
|
default = ["tls"]
|
||||||
|
|
|
@ -347,6 +347,7 @@ async function setupIndexPage() {
|
||||||
const $download = document.querySelector(".download");
|
const $download = document.querySelector(".download");
|
||||||
$download.href = baseUrl() + "?zip";
|
$download.href = baseUrl() + "?zip";
|
||||||
$download.title = "Download folder as a .zip file";
|
$download.title = "Download folder as a .zip file";
|
||||||
|
$download.classList.add("dlwt");
|
||||||
$download.classList.remove("hidden");
|
$download.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,6 +368,10 @@ async function setupIndexPage() {
|
||||||
|
|
||||||
renderPathsTableHead();
|
renderPathsTableHead();
|
||||||
renderPathsTableBody();
|
renderPathsTableBody();
|
||||||
|
|
||||||
|
if (DATA.user) {
|
||||||
|
setupDownloadWithToken();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -449,13 +454,13 @@ function addPath(file, index) {
|
||||||
if (DATA.allow_archive) {
|
if (DATA.allow_archive) {
|
||||||
actionDownload = `
|
actionDownload = `
|
||||||
<div class="action-btn">
|
<div class="action-btn">
|
||||||
<a href="${url}?zip" title="Download folder as a .zip file">${ICONS.download}</a>
|
<a class="dlwt" href="${url}?zip" title="Download folder as a .zip file" download>${ICONS.download}</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
actionDownload = `
|
actionDownload = `
|
||||||
<div class="action-btn" >
|
<div class="action-btn" >
|
||||||
<a href="${url}" title="Download file" download>${ICONS.download}</a>
|
<a class="dlwt" href="${url}" title="Download file" download>${ICONS.download}</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (DATA.allow_delete) {
|
if (DATA.allow_delete) {
|
||||||
|
@ -536,6 +541,33 @@ async function setupAuth() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupDownloadWithToken() {
|
||||||
|
document.querySelectorAll("a.dlwt").forEach(link => {
|
||||||
|
link.addEventListener("click", async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const link = e.currentTarget || e.target;
|
||||||
|
const originalHref = link.getAttribute("href");
|
||||||
|
const tokengenUrl = new URL(originalHref);
|
||||||
|
tokengenUrl.searchParams.set("tokengen", "");
|
||||||
|
const res = await fetch(tokengenUrl);
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch token");
|
||||||
|
const token = await res.text();
|
||||||
|
const downloadUrl = new URL(originalHref);
|
||||||
|
downloadUrl.searchParams.set("token", token);
|
||||||
|
const tempA = document.createElement("a");
|
||||||
|
tempA.href = downloadUrl.toString();
|
||||||
|
tempA.download = "";
|
||||||
|
document.body.appendChild(tempA);
|
||||||
|
tempA.click();
|
||||||
|
document.body.removeChild(tempA);
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Failed to download, ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setupSearch() {
|
function setupSearch() {
|
||||||
const $searchbar = document.querySelector(".searchbar");
|
const $searchbar = document.querySelector(".searchbar");
|
||||||
$searchbar.classList.remove("hidden");
|
$searchbar.classList.remove("hidden");
|
||||||
|
@ -646,7 +678,7 @@ async function setupEditorPage() {
|
||||||
$editor.value = decoder.decode(dataView);
|
$editor.value = decoder.decode(dataView);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Failed get file, ${err.message}`);
|
alert(`Failed to get file, ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
69
src/auth.rs
69
src/auth.rs
|
@ -2,11 +2,13 @@ use crate::{args::Args, server::Response, utils::unix_now};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
|
use ed25519_dalek::{ed25519::signature::SignerMut, Signature, SigningKey};
|
||||||
use headers::HeaderValue;
|
use headers::HeaderValue;
|
||||||
use hyper::{header::WWW_AUTHENTICATE, Method};
|
use hyper::{header::WWW_AUTHENTICATE, Method};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use md5::Context;
|
use md5::Context;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -14,7 +16,8 @@ use std::{
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const REALM: &str = "DUFS";
|
const REALM: &str = "DUFS";
|
||||||
const DIGEST_AUTH_TIMEOUT: u32 = 604800; // 7 days
|
const DIGEST_AUTH_TIMEOUT: u32 = 60 * 60 * 24 * 7; // 7 days
|
||||||
|
const TOKEN_EXPIRATION: u64 = 1000 * 60 * 60 * 24 * 3; // 3 days
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref NONCESTARTHASH: Context = {
|
static ref NONCESTARTHASH: Context = {
|
||||||
|
@ -105,11 +108,21 @@ impl AccessControl {
|
||||||
path: &str,
|
path: &str,
|
||||||
method: &Method,
|
method: &Method,
|
||||||
authorization: Option<&HeaderValue>,
|
authorization: Option<&HeaderValue>,
|
||||||
|
token: Option<&String>,
|
||||||
guard_options: bool,
|
guard_options: bool,
|
||||||
) -> (Option<String>, Option<AccessPaths>) {
|
) -> (Option<String>, Option<AccessPaths>) {
|
||||||
if self.users.is_empty() {
|
if self.users.is_empty() {
|
||||||
return (None, Some(AccessPaths::new(AccessPerm::ReadWrite)));
|
return (None, Some(AccessPaths::new(AccessPerm::ReadWrite)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if method == Method::GET {
|
||||||
|
if let Some(token) = token {
|
||||||
|
if let Ok((user, ap)) = self.verifty_token(token, path) {
|
||||||
|
return (Some(user), ap.guard(path, method));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(authorization) = authorization {
|
if let Some(authorization) = authorization {
|
||||||
if let Some(user) = get_auth_user(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) {
|
||||||
|
@ -135,6 +148,49 @@ impl AccessControl {
|
||||||
|
|
||||||
(None, None)
|
(None, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_token(&self, path: &str, user: &str) -> Result<String> {
|
||||||
|
let (pass, _) = self
|
||||||
|
.users
|
||||||
|
.get(user)
|
||||||
|
.ok_or_else(|| anyhow!("Not found user '{user}'"))?;
|
||||||
|
let exp = unix_now().as_millis() as u64 + TOKEN_EXPIRATION;
|
||||||
|
let message = format!("{path}:{exp}");
|
||||||
|
let mut signing_key = derive_secret_key(user, pass);
|
||||||
|
let sig = signing_key.sign(message.as_bytes()).to_bytes();
|
||||||
|
|
||||||
|
let mut raw = Vec::with_capacity(64 + 8 + user.len());
|
||||||
|
raw.extend_from_slice(&sig);
|
||||||
|
raw.extend_from_slice(&exp.to_be_bytes());
|
||||||
|
raw.extend_from_slice(user.as_bytes());
|
||||||
|
|
||||||
|
Ok(hex::encode(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verifty_token<'a>(&'a self, token: &str, path: &str) -> Result<(String, &'a AccessPaths)> {
|
||||||
|
let raw = hex::decode(token)?;
|
||||||
|
|
||||||
|
let sig_bytes = &raw[..64];
|
||||||
|
let exp_bytes = &raw[64..72];
|
||||||
|
let user_bytes = &raw[72..];
|
||||||
|
|
||||||
|
let exp = u64::from_be_bytes(exp_bytes.try_into()?);
|
||||||
|
if unix_now().as_millis() as u64 > exp {
|
||||||
|
bail!("Token expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = std::str::from_utf8(user_bytes)?;
|
||||||
|
let (pass, ap) = self
|
||||||
|
.users
|
||||||
|
.get(user)
|
||||||
|
.ok_or_else(|| anyhow!("Not found user '{user}'"))?;
|
||||||
|
|
||||||
|
let sig = Signature::from_bytes(&<[u8; 64]>::try_from(sig_bytes)?);
|
||||||
|
|
||||||
|
let message = format!("{path}:{exp}");
|
||||||
|
derive_secret_key(user, pass).verify(message.as_bytes(), &sig)?;
|
||||||
|
Ok((user.to_string(), ap))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
@ -422,6 +478,13 @@ pub fn check_auth(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn derive_secret_key(user: &str, pass: &str) -> SigningKey {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(format!("{user}:{pass}").as_bytes());
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
SigningKey::from_bytes(&hash.into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a nonce is still valid.
|
/// Check if a nonce is still valid.
|
||||||
/// Return an error if it was never valid
|
/// Return an error if it was never valid
|
||||||
fn validate_nonce(nonce: &[u8]) -> Result<bool> {
|
fn validate_nonce(nonce: &[u8]) -> Result<bool> {
|
||||||
|
@ -433,7 +496,7 @@ fn validate_nonce(nonce: &[u8]) -> Result<bool> {
|
||||||
//get time
|
//get time
|
||||||
if let Ok(secs_nonce) = u32::from_str_radix(&n[..8], 16) {
|
if let Ok(secs_nonce) = u32::from_str_radix(&n[..8], 16) {
|
||||||
//check time
|
//check time
|
||||||
let now = unix_now()?;
|
let now = unix_now();
|
||||||
let secs_now = now.as_secs() as u32;
|
let secs_now = now.as_secs() as u32;
|
||||||
|
|
||||||
if let Some(dur) = secs_now.checked_sub(secs_nonce) {
|
if let Some(dur) = secs_now.checked_sub(secs_nonce) {
|
||||||
|
@ -513,7 +576,7 @@ fn to_headermap(header: &[u8]) -> Result<HashMap<&[u8], &[u8]>, ()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_nonce() -> Result<String> {
|
fn create_nonce() -> Result<String> {
|
||||||
let now = unix_now()?;
|
let now = unix_now();
|
||||||
let secs = now.as_secs() as u32;
|
let secs = now.as_secs() as u32;
|
||||||
let mut h = NONCESTARTHASH.clone();
|
let mut h = NONCESTARTHASH.clone();
|
||||||
h.consume(secs.to_be_bytes());
|
h.consume(secs.to_be_bytes());
|
||||||
|
|
|
@ -149,20 +149,6 @@ impl Server {
|
||||||
let headers = req.headers();
|
let headers = req.headers();
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
|
|
||||||
let user_agent = headers
|
|
||||||
.get("user-agent")
|
|
||||||
.and_then(|v| v.to_str().ok())
|
|
||||||
.map(|v| v.to_lowercase())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let is_microsoft_webdav = user_agent.starts_with("microsoft-webdav-miniredir/");
|
|
||||||
|
|
||||||
if is_microsoft_webdav {
|
|
||||||
// microsoft webdav requires this.
|
|
||||||
res.headers_mut()
|
|
||||||
.insert(CONNECTION, HeaderValue::from_static("close"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let relative_path = match self.resolve_path(req_path) {
|
let relative_path = match self.resolve_path(req_path) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => {
|
||||||
|
@ -179,11 +165,34 @@ impl Server {
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let user_agent = headers
|
||||||
|
.get("user-agent")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.map(|v| v.to_lowercase())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let is_microsoft_webdav = user_agent.starts_with("microsoft-webdav-miniredir/");
|
||||||
|
|
||||||
|
if is_microsoft_webdav {
|
||||||
|
// microsoft webdav requires this.
|
||||||
|
res.headers_mut()
|
||||||
|
.insert(CONNECTION, HeaderValue::from_static("close"));
|
||||||
|
}
|
||||||
|
|
||||||
let authorization = headers.get(AUTHORIZATION);
|
let authorization = headers.get(AUTHORIZATION);
|
||||||
let guard =
|
|
||||||
self.args
|
let query = req.uri().query().unwrap_or_default();
|
||||||
.auth
|
let mut query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
|
||||||
.guard(&relative_path, &method, authorization, is_microsoft_webdav);
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let guard = self.args.auth.guard(
|
||||||
|
&relative_path,
|
||||||
|
&method,
|
||||||
|
authorization,
|
||||||
|
query_params.get("token"),
|
||||||
|
is_microsoft_webdav,
|
||||||
|
);
|
||||||
|
|
||||||
let (user, access_paths) = match guard {
|
let (user, access_paths) = match guard {
|
||||||
(None, None) => {
|
(None, None) => {
|
||||||
|
@ -197,11 +206,6 @@ impl Server {
|
||||||
(x, Some(y)) => (x, y),
|
(x, Some(y)) => (x, y),
|
||||||
};
|
};
|
||||||
|
|
||||||
let query = req.uri().query().unwrap_or_default();
|
|
||||||
let mut query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
|
|
||||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if detect_noscript(&user_agent) {
|
if detect_noscript(&user_agent) {
|
||||||
query_params.insert("noscript".to_string(), String::new());
|
query_params.insert("noscript".to_string(), String::new());
|
||||||
}
|
}
|
||||||
|
@ -214,6 +218,11 @@ impl Server {
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if has_query_flag(&query_params, "tokengen") {
|
||||||
|
self.handle_tokengen(&relative_path, user, &mut res).await?;
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
let head_only = method == Method::HEAD;
|
let head_only = method == Method::HEAD;
|
||||||
|
|
||||||
if self.args.path_is_file {
|
if self.args.path_is_file {
|
||||||
|
@ -996,6 +1005,24 @@ impl Server {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_tokengen(
|
||||||
|
&self,
|
||||||
|
relative_path: &str,
|
||||||
|
user: Option<String>,
|
||||||
|
res: &mut Response,
|
||||||
|
) -> Result<()> {
|
||||||
|
let output = self
|
||||||
|
.args
|
||||||
|
.auth
|
||||||
|
.generate_token(relative_path, &user.unwrap_or_default())?;
|
||||||
|
res.headers_mut()
|
||||||
|
.typed_insert(ContentType::from(mime_guess::mime::TEXT_PLAIN_UTF_8));
|
||||||
|
res.headers_mut()
|
||||||
|
.typed_insert(ContentLength(output.len() as u64));
|
||||||
|
*res.body_mut() = body_full(output);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_propfind_dir(
|
async fn handle_propfind_dir(
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
@ -1271,7 +1298,7 @@ impl Server {
|
||||||
let guard = self
|
let guard = self
|
||||||
.args
|
.args
|
||||||
.auth
|
.auth
|
||||||
.guard(&dest_path, req.method(), authorization, false);
|
.guard(&dest_path, req.method(), authorization, None, false);
|
||||||
|
|
||||||
match guard {
|
match guard {
|
||||||
(_, Some(_)) => {}
|
(_, Some(_)) => {}
|
||||||
|
|
|
@ -8,10 +8,10 @@ use std::{
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn unix_now() -> Result<Duration> {
|
pub fn unix_now() -> Duration {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.with_context(|| "Invalid system time")
|
.expect("Unable to get unix epoch time")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode_uri(v: &str) -> String {
|
pub fn encode_uri(v: &str) -> String {
|
||||||
|
|
|
@ -348,3 +348,19 @@ fn auth_shadow(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn token_auth(#[with(&["-a", "user:pass@/"])] server: TestServer) -> Result<(), Error> {
|
||||||
|
let url = format!("{}index.html", server.url());
|
||||||
|
let resp = fetch!(b"GET", &url).send()?;
|
||||||
|
assert_eq!(resp.status(), 401);
|
||||||
|
let url = format!("{}index.html?tokengen", server.url());
|
||||||
|
let resp = fetch!(b"GET", &url)
|
||||||
|
.basic_auth("user", Some("pass"))
|
||||||
|
.send()?;
|
||||||
|
let token = resp.text()?;
|
||||||
|
let url = format!("{}index.html?token={token}", server.url());
|
||||||
|
let resp = fetch!(b"GET", &url).send()?;
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue