Authenticate configured WebDAV users with omitted empty passwords

This commit is contained in:
Danny Kim
2026-09-30 15:28:48 +09:00
parent b796cd4622
commit 4ec7e53ab5
+66 -27
View File
@@ -26,6 +26,28 @@ struct Session {
authorization: Option<String>,
}
impl Session {
fn from_config(config: &[(String, Setting)]) -> Result<Self, ErrorCode> {
let lookup = |wanted: &str| {
config
.iter()
.find(|(key, _)| key == wanted)
.and_then(|(_, value)| text(Some(value.clone())))
};
let root = lookup("url").ok_or(ErrorCode::InvalidInput)?;
if !root.starts_with("https://") && !root.starts_with("http://") {
return Err(ErrorCode::InvalidInput);
}
let authorization = lookup("username").map(|username| {
dav::basic(&username, &lookup("password").unwrap_or_default())
});
Ok(Self {
root,
authorization,
})
}
}
thread_local! {
static SESSION: RefCell<Option<Session>> = const { RefCell::new(None) };
}
@@ -227,26 +249,7 @@ impl Guest for Webdav {
config: Vec<(String, Setting)>,
) -> Result<Capabilities, ErrorCode> {
SESSION.set(None);
let lookup = |wanted: &str| {
config
.iter()
.find(|(key, _)| key == wanted)
.and_then(|(_, value)| text(Some(value.clone())))
};
let root = lookup("url").ok_or(ErrorCode::InvalidInput)?;
if !root.starts_with("https://") && !root.starts_with("http://") {
return Err(ErrorCode::InvalidInput);
}
let authorization = match (lookup("username"), lookup("password")) {
(Some(username), Some(password)) => {
Some(dav::basic(&username, &password))
}
_ => None,
};
SESSION.set(Some(Session {
root,
authorization,
}));
SESSION.set(Some(Session::from_config(&config)?));
let probe = Self::stat(Locator::Path("/".to_owned())).and_then(|info| {
if matches!(info.kind, EntryKind::Directory) {
Ok(())
@@ -429,23 +432,59 @@ sdk::export!(Webdav with_types_in sdk);
#[cfg(test)]
mod tests {
use super::{SESSION, Session, Webdav, text};
use super::{SESSION, Session, Webdav};
use crate::sdk::{ErrorCode, Guest, Setting};
#[tokio::test]
async fn passwords_keep_leading_and_trailing_whitespace() {
async fn passwords_keep_leading_and_trailing_whitespace()
-> Result<(), ErrorCode> {
let config = vec![
(
"url".to_owned(),
Setting::Text("https://dav.example/".to_owned()),
),
("username".to_owned(), Setting::Text("user".to_owned())),
(
"password".to_owned(),
Setting::Secret(" pass word \t".to_owned()),
),
];
assert_eq!(
text(Some(Setting::Secret(" pass word \t".to_owned()))),
Some(" pass word \t".to_owned())
Session::from_config(&config)?.authorization.as_deref(),
Some("Basic dXNlcjogcGFzcyB3b3JkIAk=")
);
Ok(())
}
#[tokio::test]
async fn an_empty_password_still_supplies_basic_auth_credentials() {
async fn a_username_without_a_password_setting_sends_basic_auth()
-> Result<(), ErrorCode> {
let config = vec![
(
"url".to_owned(),
Setting::Text("https://dav.example/".to_owned()),
),
("username".to_owned(), Setting::Text("user".to_owned())),
];
assert_eq!(
text(Some(Setting::Secret(String::new()))),
Some(String::new())
Session::from_config(&config)?.authorization.as_deref(),
Some("Basic dXNlcjo=")
);
Ok(())
}
#[tokio::test]
async fn no_username_keeps_the_connection_anonymous() -> Result<(), ErrorCode>
{
let config = vec![
(
"url".to_owned(),
Setting::Text("https://dav.example/".to_owned()),
),
("password".to_owned(), Setting::Secret("unused".to_owned())),
];
assert!(Session::from_config(&config)?.authorization.is_none());
Ok(())
}
#[tokio::test]