From bae6e3dd6659b6a0b5108ef6775d0e3633e7f931 Mon Sep 17 00:00:00 2001 From: Danny Kim Date: Wed, 30 Sep 2026 15:34:12 +0900 Subject: [PATCH] Retain canonical WebDAV root href after alias redirects --- src/lib.rs | 110 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d8bedba..d408824 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ const MAX_CHUNK: u32 = 1024 * 1024; struct Session { root: String, + /// The server's canonical href is only used to identify listing responses. + root_href: String, authorization: Option, } @@ -42,10 +44,20 @@ impl Session { dav::basic(&username, &lookup("password").unwrap_or_default()) }); Ok(Self { + root_href: root.clone(), root, authorization, }) } + + fn record_root(&mut self, found: &[dav::Found]) -> Result<(), ErrorCode> { + let root = found.first().ok_or(ErrorCode::NotFound)?; + if !root.collection || root.href.is_empty() { + return Err(ErrorCode::InvalidInput); + } + self.root_href.clone_from(&root.href); + Ok(()) + } } thread_local! { @@ -250,12 +262,13 @@ impl Guest for Webdav { ) -> Result { SESSION.set(None); 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(()) - } else { - Err(ErrorCode::InvalidInput) - } + let probe = describe("/", "0").and_then(|found| { + SESSION.with_borrow_mut(|session| { + session + .as_mut() + .ok_or(ErrorCode::Internal)? + .record_root(&found) + }) }); if let Err(error) = probe { SESSION.set(None); @@ -297,7 +310,7 @@ impl Guest for Webdav { None => 0, }; let found = describe(&path, "1")?; - let url = session(|session| Ok(dav::url(&session.root, &path)))?; + let url = session(|session| Ok(dav::url(&session.root_href, &path)))?; let children: Vec<_> = dav::children(&found, &url)? .iter() .map(|entry| Entry { @@ -417,7 +430,7 @@ impl Guest for Webdav { /// first and refused unless it is empty. fn remove(target: Locator) -> Result<(), ErrorCode> { let path = route(&target); - let url = session(|session| Ok(dav::url(&session.root, &path)))?; + let url = session(|session| Ok(dav::url(&session.root_href, &path)))?; dav::removable(&describe(&path, "1")?, &url)?; exchange(host::Method::Delete, &path, Vec::new(), &[])?; Ok(()) @@ -433,6 +446,7 @@ sdk::export!(Webdav with_types_in sdk); #[cfg(test)] mod tests { use super::{SESSION, Session, Webdav}; + use crate::dav; use crate::sdk::{ErrorCode, Guest, Setting}; #[tokio::test] @@ -487,10 +501,90 @@ mod tests { Ok(()) } + #[tokio::test] + async fn a_redirected_root_lists_its_canonical_directory() + -> Result<(), ErrorCode> { + let config = vec![( + "url".to_owned(), + Setting::Text("https://dav.example/webdav".to_owned()), + )]; + let mut session = Session::from_config(&config)?; + let entries = [ + dav::Found { + href: "/files/notes.md".to_owned(), + collection: false, + length: 12, + etag: None, + }, + dav::Found { + href: "/files/".to_owned(), + collection: true, + length: 0, + etag: None, + }, + ]; + session.record_root(&entries[1..])?; + let expected_url = dav::url(&session.root_href, "/"); + let listed = dav::children(&entries, &expected_url)?; + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].href, "/files/notes.md"); + + let nested = [ + dav::Found { + href: "/files/documents/child.md".to_owned(), + collection: false, + length: 7, + etag: None, + }, + dav::Found { + href: "/files/documents/".to_owned(), + collection: true, + length: 0, + etag: None, + }, + ]; + let expected_url = dav::url(&session.root_href, "/documents"); + let listed = dav::children(&nested, &expected_url)?; + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].href, "/files/documents/child.md"); + assert_eq!( + dav::removable(&nested, &expected_url), + Err(ErrorCode::Conflict) + ); + Ok(()) + } + + #[tokio::test] + async fn a_canonical_href_does_not_change_the_authenticated_request_origin() + -> Result<(), ErrorCode> { + let config = vec![ + ( + "url".to_owned(), + Setting::Text("https://dav.example/webdav".to_owned()), + ), + ("username".to_owned(), Setting::Text("user".to_owned())), + ]; + let mut session = Session::from_config(&config)?; + session.record_root(&[dav::Found { + href: "https://other.example/files/".to_owned(), + collection: true, + length: 0, + etag: None, + }])?; + assert_eq!(dav::url(&session.root, "/"), "https://dav.example/webdav"); + assert_eq!( + dav::url(&session.root, "/documents/child.md"), + "https://dav.example/webdav/documents/child.md" + ); + assert_eq!(session.authorization.as_deref(), Some("Basic dXNlcjo=")); + Ok(()) + } + #[tokio::test] async fn a_failed_reopen_discards_the_previous_connection() { SESSION.set(Some(Session { root: "https://dav.example/files/".to_owned(), + root_href: "/files/".to_owned(), authorization: None, })); let result = Webdav::open("webdav".to_owned(), Vec::new());