dav.rs
⎇
Raw
1//! WebDAV endpoint.
2//!
3//! Two mounts, both served by the same [`FbFs`]:
4//!
5//! * `{DAV}` — a signed-in user's roots. Each root is a child collection of a
6//! synthetic top-level directory, so one mount covers every root the user
7//! has.
8//! * `{DAV_SHARE}/{token}` — one public share, mounted at its own root.
9//!
10//! All filesystem access goes through [`crate::fs`], so a mount inherits the
11//! same containment and the same symlink handling the JSON API has.
12//!
13//! The protocol itself (PROPFIND, the 207 multistatus, `Depth`, `Destination`,
14//! `Overwrite`, conditional headers) is `dav-server`'s job. This module only
15//! authenticates the request, decides which roots it may see, and maps dav
16//! paths onto real ones.
17
18use std::collections::HashMap;
19use std::io::SeekFrom;
20use std::path::{Path, PathBuf};
21use std::sync::{Arc, LazyLock, Mutex, Weak};
22use std::time::{Duration, SystemTime, UNIX_EPOCH};
23
24use api_types::{AuthMode, DAV, DAV_SHARE, Mode};
25use axum::body::Body;
26use axum::extract::State;
27use axum::http::header::{HeaderMap, WWW_AUTHENTICATE};
28use axum::http::{Request, Response, StatusCode};
29use axum::response::IntoResponse;
30use bytes::{Buf, Bytes};
31use dav_server::DavConfig;
32use dav_server::davpath::{DavPath, ParseError};
33use dav_server::fs::{
34 DavDirEntry, DavFile, DavMetaData, FsError, FsFuture, FsResult, FsStream, GuardedFileSystem,
35 OpenOptions, ReadDirMeta,
36};
37use dav_server::ls::{DavLock, DavLockSystem, LsFuture};
38use dav_server::memls::MemLs;
39use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
40
41use crate::api::common::{display_name, session_auth};
42use crate::auth;
43use crate::db::RootRow;
44use crate::error::AppState;
45
46/// The `WWW-Authenticate` realm. Clients show it in their password prompt.
47const REALM: &str = "filebrowser-ng";
48
49// ---------------------------------------------------------------------------
50// Routes
51// ---------------------------------------------------------------------------
52
53/// `{DAV}` and everything under it: the signed-in user's roots.
54///
55/// A browser session cookie is accepted, but the usual caller is a mount
56/// client, which only speaks HTTP Basic.
57pub async fn user(State(state): State<Arc<AppState>>, req: Request<Body>) -> Response<Body> {
58 let (principal, roots) = match authenticate(&state, req.headers()).await {
59 Some(v) => v,
60 None => return challenge(),
61 };
62 // The admin pseudo-root (the whole server root, read-only) is deliberately
63 // not mounted: `session_auth` does not add it, and a mount that silently
64 // contained a second copy of every other root would be confusing.
65 let mount = Mount {
66 roots: Arc::new(root_segments(&state, roots)),
67 flat: false,
68 };
69 serve(state, req, DAV.to_string(), principal, mount).await
70}
71
72/// `{DAV_SHARE}/{token}` and everything under it: one public share.
73///
74/// Folder shares only. A file share has no collection to mount, and its one
75/// file is already a plain `GET` away on the share page.
76pub async fn share(State(state): State<Arc<AppState>>, req: Request<Body>) -> Response<Body> {
77 let Some(token) = share_token(req.uri().path()) else {
78 return StatusCode::NOT_FOUND.into_response();
79 };
80 let row = match state.db.share_by_token(&token).await {
81 Ok(Some(row)) => row,
82 Ok(None) => return StatusCode::NOT_FOUND.into_response(),
83 Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
84 };
85 if row.is_expired() {
86 return StatusCode::GONE.into_response();
87 }
88 if row.is_file {
89 return StatusCode::NOT_FOUND.into_response();
90 }
91 // A protected share takes its password over Basic, with the user name
92 // ignored. There is no account behind a share link to name.
93 if let Some(hash) = row.password_hash.clone() {
94 let Some((_, password)) = auth::basic_credentials(req.headers()) else {
95 return challenge();
96 };
97 let (pw, id, tok) = (password.clone(), row.id, token.clone());
98 let ok = auth::verify_cached(row.id, "", &password, move || async move {
99 // Throttled like `POST /api/share/{token}/unlock`, keyed the same
100 // way, so a mount client is not the cheap way to guess.
101 let delay = auth::login_delay(&tok);
102 if !delay.is_zero() {
103 tokio::time::sleep(delay).await;
104 }
105 let ok = auth::verify_password_async(&pw, &hash).await;
106 auth::record_login(&tok, ok);
107 ok.then_some(id)
108 })
109 .await;
110 if ok.is_none() {
111 return challenge();
112 }
113 }
114 let root = RootRow {
115 id: row.id,
116 path: row.target.clone(),
117 mode: row.mode,
118 };
119 let mount = Mount {
120 roots: Arc::new(vec![(String::new(), root)]),
121 flat: true,
122 };
123 let prefix = format!("{DAV_SHARE}/{token}");
124 serve(state, req, prefix, format!("share-{}", row.id), mount).await
125}
126
127/// Whether the request path still addresses this mount after `dav-server` has
128/// normalized it: percent-decoded, `.` and `..` resolved, slashes merged.
129///
130/// Runs the same two steps as the handler, so it rejects nothing the handler
131/// would accept. The other parse errors (`InvalidPath`, `ForbiddenPath`) are
132/// left to the handler, which answers those with a 4xx of its own.
133fn path_in_mount(path: &str, prefix: &str) -> bool {
134 // `OPTIONS *` has no leading slash. The handler answers it.
135 if !path.starts_with('/') {
136 return true;
137 }
138 !matches!(
139 DavPath::new(path).and_then(|mut p| p.set_prefix(prefix)),
140 Err(ParseError::PrefixMismatch)
141 )
142}
143
144/// The `Destination` header as a URL path, the way `dav-server`'s private
145/// header parser reads it: a path as-is, a full URL (what mount clients send)
146/// reduced to its path. `None` for a missing or unparseable header.
147fn destination_path(headers: &HeaderMap) -> Option<String> {
148 let raw = headers.get("destination")?.to_str().ok()?;
149 if raw.starts_with('/') {
150 return Some(raw.to_string());
151 }
152 raw.parse::<axum::http::Uri>()
153 .ok()
154 .map(|u| u.path().to_string())
155}
156
157/// The token out of the *raw* URL path.
158///
159/// Not axum's decoded wildcard: `DavPath` keeps the raw path, and
160/// `strip_prefix` byte-compares against it. A decoded `<token>%2Fx` would
161/// yield a prefix that the dav path does not start with.
162fn share_token(path: &str) -> Option<String> {
163 let rest = path.strip_prefix(DAV_SHARE)?.strip_prefix('/')?;
164 let token = rest.split('/').next().unwrap_or_default();
165 (!token.is_empty()).then(|| token.to_string())
166}
167
168/// Hand the request to `dav-server` and, afterwards, keep the share table in
169/// step with the filesystem.
170///
171/// The handler is built here rather than once at startup because `prefix`
172/// differs per share mount, and `dav-server` only allows a per-request config
173/// override on the unguarded handler. Building it is cheap: an `Arc::new` and
174/// two `Arc`-backed trait-object clones.
175async fn serve(
176 state: Arc<AppState>,
177 req: Request<Body>,
178 prefix: String,
179 principal: String,
180 mount: Mount,
181) -> Response<Body> {
182 // `dav-server` refuses an escaping path too, but with `502 Bad Gateway`
183 // (`DavError::IllegalPath`), which reads as a broken upstream. A `..` that
184 // stays inside the mount is already a `403`, so answer this the same way.
185 // The same for a COPY or MOVE `Destination`, which the handler normalizes
186 // the same way. A missing or malformed header stays with the handler.
187 let dest_in_mount =
188 destination_path(req.headers()).is_none_or(|dest| path_in_mount(&dest, &prefix));
189 if !path_in_mount(req.uri().path(), &prefix) || !dest_in_mount {
190 return StatusCode::FORBIDDEN.into_response();
191 }
192
193 // Resolved *before* the operation, while the item still exists: once
194 // DELETE or MOVE has run there is no path left to look a share up by.
195 let vacating = matches!(req.method().as_str(), "DELETE" | "MOVE");
196 let vacated = if vacating {
197 dav_target(&state, &mount, &prefix, &req)
198 } else {
199 None
200 };
201
202 let handler = DavConfig::<Mount>::new()
203 .filesystem(Box::new(FbFs {
204 state: state.clone(),
205 }))
206 // The handler is rebuilt per request, the lock tree must not be.
207 .locksystem(Box::new(locks_for(&principal)))
208 // Its only effect in `dav-server` is picking the `ReadDirMeta` for
209 // PROPFIND. `false` keeps listings on the followed metadata.
210 .hide_symlinks(false)
211 // Off by default in `dav-server`: without it a plain `GET` of any
212 // collection answers 405, so the mount is unreadable in a browser.
213 .autoindex(true)
214 .strip_prefix(prefix)
215 .build_handler();
216
217 let mut resp = handler.handle_guarded(req, principal, mount).await;
218 crate::api::sandbox_scriptable(&mut resp);
219
220 // One revoke for the whole request. Doing it inside the filesystem would
221 // fire a query per removed item, and a recursive DELETE walks the tree.
222 if let Some(abs) = vacated
223 && resp.status().is_success()
224 {
225 crate::api::files::revoke_shares_at(&state, &abs).await;
226 }
227 resp.map(Body::new)
228}
229
230/// The absolute path a request addresses, if it resolves to one today.
231fn dav_target(
232 state: &AppState,
233 mount: &Mount,
234 prefix: &str,
235 req: &Request<Body>,
236) -> Option<PathBuf> {
237 // `DavPath::new`, not `from_uri`: the latter keeps the raw bytes, so an
238 // encoded path (`a%20b.txt`) would never resolve and the share would
239 // outlive the file it named.
240 let mut path = DavPath::new(req.uri().path()).ok()?;
241 path.set_prefix(prefix).ok()?;
242 let (root, rel) = item(&path, mount).ok()?;
243 // `resolve_entry`, matching the operations this is predicting. Following
244 // the last component would name a symlink's target, so deleting a link
245 // would revoke a share on a file that is still there.
246 crate::fs::resolve_entry(&state.root, &root.path, &rel).ok()
247}
248
249/// 401 with the Basic challenge every mount client needs to see before it
250/// will send credentials at all.
251fn challenge() -> Response<Body> {
252 (
253 StatusCode::UNAUTHORIZED,
254 [(WWW_AUTHENTICATE, format!("Basic realm=\"{REALM}\""))],
255 )
256 .into_response()
257}
258
259// ---------------------------------------------------------------------------
260// Authentication
261// ---------------------------------------------------------------------------
262
263/// Resolve the caller to a principal name and the roots they may mount.
264async fn authenticate(state: &AppState, headers: &HeaderMap) -> Option<(String, Vec<RootRow>)> {
265 // A browser hitting the mount already has a session; take it and skip
266 // Argon2 entirely.
267 if auth::parse_session_cookie(headers).is_some()
268 && let Ok((user, roots)) = session_auth(headers, state).await
269 {
270 return Some((user.name, roots));
271 }
272
273 let (name, password) = auth::basic_credentials(headers)?;
274 let id = auth::verify_cached(0, &name, &password, || {
275 let (state, name, password) = (state, name.clone(), password.clone());
276 async move {
277 // The login route's throttle, keyed the same way, so guessing over
278 // WebDAV is no cheaper than guessing over the login form.
279 let delay = auth::login_delay(&name);
280 if !delay.is_zero() {
281 tokio::time::sleep(delay).await;
282 }
283 let verified = state.db.verify_password(&name, &password).await.ok()?;
284 // Record what the password did, not what the rule below decides.
285 // A mount on an account that requires a passkey keeps retrying,
286 // and counting each retry as a failed guess would pin that name's
287 // delay and lock the person out of the web login too.
288 auth::record_login(&name, verified.is_some());
289 // Basic carries a password and nothing else, so an account that
290 // requires a passkey too cannot be authenticated over WebDAV.
291 // Accepting it would quietly downgrade the account's own setting.
292 verified
293 .filter(|u| u.auth_mode == AuthMode::Either)
294 .map(|u| u.id)
295 }
296 })
297 .await?;
298 let roots = state.db.user_roots(id).await.ok()?;
299 Some((name, roots))
300}
301
302// ---------------------------------------------------------------------------
303// Locking
304// ---------------------------------------------------------------------------
305
306/// One lock tree per principal.
307///
308/// A lock is keyed by DAV URL, and a URL segment is a root's display name, so a
309/// single shared tree lets two users whose roots are both named `Documents`
310/// reach each other's locks. One could block the other, and `PROPFIND` hands
311/// back the holder's lock token, which is enough to release that lock or write
312/// through it.
313///
314/// The cost is that two principals sharing one physical folder do not
315/// coordinate through WebDAV locks. Byte-level safety does not rest on this:
316/// [`WRITE_LOCKS`] keys on the resolved path and covers every writer.
317///
318/// `MemLs` keeps its state in an `Arc`, so a clone shares that principal's
319/// tree. Locks live in memory only, and a restart drops them all, which is
320/// what a client already sees when a lock times out.
321static LOCKS: LazyLock<Mutex<HashMap<String, ExpiringLs>>> =
322 LazyLock::new(|| Mutex::new(HashMap::new()));
323
324fn locks_for(principal: &str) -> ExpiringLs {
325 let mut g = LOCKS.lock().unwrap_or_else(|e| e.into_inner());
326 g.entry(principal.to_string())
327 .or_insert_with(|| ExpiringLs(*MemLs::new()))
328 .clone()
329}
330
331/// Longest lock handed out, and so the longest an abandoned one blocks a file.
332/// A client that still wants the file refreshes; one that crashed does not.
333///
334/// Matches `dav-server`'s own ceiling for an exclusive lock. It caps only the
335/// two cases that arrive here uncapped, both as `None` meaning "never
336/// expires": a LOCK with no `Timeout` header, and a refresh asking for
337/// `Infinite`.
338const LOCK_TIMEOUT: Duration = Duration::from_secs(600);
339
340/// [`MemLs`] with lock expiry actually applied.
341///
342/// `MemLs` records a lock's `timeout_at` and then never looks at it again, and
343/// it honours an infinite timeout request. Left alone, a client that died
344/// holding an exclusive lock would block that file until the process restarts.
345/// Every call here first drops the expired locks covering the path it touches,
346/// and no lock is granted for longer than [`LOCK_TIMEOUT`].
347#[derive(Debug, Clone)]
348struct ExpiringLs(MemLs);
349
350impl ExpiringLs {
351 /// Drop the expired locks on `path` and its ancestors.
352 ///
353 /// Only the locks that could block an operation *on this path*. A lock on
354 /// a descendant is not swept, so a deep operation can still be refused by
355 /// a stale lock below it until something touches that path directly.
356 async fn sweep(&self, path: &DavPath) {
357 let now = SystemTime::now();
358 for lock in self.0.discover(path).await {
359 if lock.timeout_at.is_some_and(|t| t <= now) {
360 let _ = self.0.unlock(&lock.path, &lock.token).await;
361 }
362 }
363 }
364
365 /// Never `None`, never longer than [`LOCK_TIMEOUT`]. `None` would mean a
366 /// lock that [`sweep`](Self::sweep) can never clear.
367 fn capped(timeout: Option<Duration>) -> Option<Duration> {
368 Some(timeout.unwrap_or(LOCK_TIMEOUT).min(LOCK_TIMEOUT))
369 }
370}
371
372impl DavLockSystem for ExpiringLs {
373 fn lock(
374 &self,
375 path: &DavPath,
376 principal: Option<&str>,
377 owner: Option<&xmltree::Element>,
378 timeout: Option<Duration>,
379 shared: bool,
380 deep: bool,
381 ) -> LsFuture<'_, Result<DavLock, DavLock>> {
382 // The borrows end with the call, not with the future, so clone into it.
383 let (path, principal) = (path.clone(), principal.map(str::to_string));
384 let owner = owner.cloned();
385 Box::pin(async move {
386 self.sweep(&path).await;
387 self.0
388 .lock(
389 &path,
390 principal.as_deref(),
391 owner.as_ref(),
392 Self::capped(timeout),
393 shared,
394 deep,
395 )
396 .await
397 })
398 }
399
400 fn unlock(&self, path: &DavPath, token: &str) -> LsFuture<'_, Result<(), ()>> {
401 let (path, token) = (path.clone(), token.to_string());
402 Box::pin(async move { self.0.unlock(&path, &token).await })
403 }
404
405 fn refresh(
406 &self,
407 path: &DavPath,
408 token: &str,
409 timeout: Option<Duration>,
410 ) -> LsFuture<'_, Result<DavLock, ()>> {
411 let (path, token) = (path.clone(), token.to_string());
412 Box::pin(async move {
413 // Swept first: a client refreshing a lock it let expire must be
414 // told, not silently handed the file back.
415 self.sweep(&path).await;
416 self.0.refresh(&path, &token, Self::capped(timeout)).await
417 })
418 }
419
420 fn check(
421 &self,
422 path: &DavPath,
423 principal: Option<&str>,
424 ignore_principal: bool,
425 deep: bool,
426 submitted_tokens: &[String],
427 ) -> LsFuture<'_, Result<(), DavLock>> {
428 let (path, principal) = (path.clone(), principal.map(str::to_string));
429 let tokens = submitted_tokens.to_vec();
430 Box::pin(async move {
431 self.sweep(&path).await;
432 self.0
433 .check(&path, principal.as_deref(), ignore_principal, deep, &tokens)
434 .await
435 })
436 }
437
438 fn discover(&self, path: &DavPath) -> LsFuture<'_, Vec<DavLock>> {
439 let path = path.clone();
440 Box::pin(async move {
441 self.sweep(&path).await;
442 self.0.discover(&path).await
443 })
444 }
445
446 fn delete(&self, path: &DavPath) -> LsFuture<'_, Result<(), ()>> {
447 let path = path.clone();
448 Box::pin(async move { self.0.delete(&path).await })
449 }
450}
451
452/// One mutex per path with a writer on it.
453///
454/// WebDAV locking does not cover this: a lock is only consulted for a client
455/// that sends LOCK, and a plain PUT never does. Two concurrent PUTs otherwise
456/// interleave into a byte-level splice of both bodies, with both clients told
457/// 2xx. Serializing the write open makes the outcome last-writer-wins.
458///
459/// Keyed by the resolved absolute path, so two mounts onto the same file share
460/// one mutex. The lock tree cannot do that: it keys on the URL, and the same
461/// file has a different URL in a user mount and in a share.
462static WRITE_LOCKS: LazyLock<Mutex<HashMap<PathBuf, Weak<tokio::sync::Mutex<()>>>>> =
463 LazyLock::new(|| Mutex::new(HashMap::new()));
464
465fn write_lock(path: &Path) -> Arc<tokio::sync::Mutex<()>> {
466 let mut map = WRITE_LOCKS.lock().unwrap_or_else(|e| e.into_inner());
467 // Drop entries whose last writer finished, so the map holds in-flight
468 // writes and not every file ever written.
469 map.retain(|_, w| w.strong_count() > 0);
470 if let Some(m) = map.get(path).and_then(Weak::upgrade) {
471 return m;
472 }
473 let m = Arc::new(tokio::sync::Mutex::new(()));
474 map.insert(path.to_path_buf(), Arc::downgrade(&m));
475 m
476}
477
478// ---------------------------------------------------------------------------
479// Mount: which roots a request sees, and where in the URL they live
480// ---------------------------------------------------------------------------
481
482/// The credentials `dav-server` carries through to [`FbFs`]: the roots this
483/// request may touch, and how they are laid out under the mount point.
484#[derive(Clone)]
485pub struct Mount {
486 /// URL segment → root. The segment is empty when `flat`.
487 roots: Arc<Vec<(String, RootRow)>>,
488 /// One root mounted directly at the mount point (a share), rather than as
489 /// a child of a synthetic collection.
490 flat: bool,
491}
492
493/// What a dav path addresses.
494enum Target {
495 /// The synthetic collection at the mount point that lists the roots.
496 Roots,
497 Item {
498 root: RootRow,
499 rel: String,
500 },
501}
502
503/// The URL segment for each root: its display name, disambiguated with the
504/// root id when two roots would otherwise claim the same one.
505fn root_segments(state: &AppState, roots: Vec<RootRow>) -> Vec<(String, RootRow)> {
506 let names: Vec<String> = roots.iter().map(|r| display_name(state, &r.path)).collect();
507 roots
508 .into_iter()
509 .zip(&names)
510 .map(|(r, name)| {
511 let taken = names.iter().filter(|n| *n == name).count() > 1;
512 let seg = match taken {
513 true => format!("{name}-{}", r.id),
514 false => name.clone(),
515 };
516 (seg, r)
517 })
518 .collect()
519}
520
521fn target(path: &DavPath, mount: &Mount) -> FsResult<Target> {
522 let rel = path.as_rel_ospath();
523 if mount.flat {
524 let (_, root) = mount.roots.first().ok_or(FsError::NotFound)?;
525 return Ok(Target::Item {
526 root: root.clone(),
527 rel: rel.to_string_lossy().into_owned(),
528 });
529 }
530 let mut parts = rel.components();
531 let Some(first) = parts.next() else {
532 return Ok(Target::Roots);
533 };
534 let seg = first.as_os_str().to_string_lossy();
535 let (_, root) = mount
536 .roots
537 .iter()
538 .find(|(s, _)| s.as_str() == seg)
539 .ok_or(FsError::NotFound)?;
540 Ok(Target::Item {
541 root: root.clone(),
542 rel: parts.collect::<PathBuf>().to_string_lossy().into_owned(),
543 })
544}
545
546/// [`target`], rejecting the synthetic collection.
547fn item(path: &DavPath, mount: &Mount) -> FsResult<(RootRow, String)> {
548 match target(path, mount)? {
549 Target::Roots => Err(FsError::Forbidden),
550 Target::Item { root, rel } => Ok((root, rel)),
551 }
552}
553
554fn writable(root: &RootRow) -> FsResult<()> {
555 match root.mode {
556 Mode::Rw => Ok(()),
557 Mode::Ro => Err(FsError::Forbidden),
558 }
559}
560
561// ---------------------------------------------------------------------------
562// The filesystem
563// ---------------------------------------------------------------------------
564
565#[derive(Clone)]
566struct FbFs {
567 state: Arc<AppState>,
568}
569
570impl GuardedFileSystem<Mount> for FbFs {
571 fn open<'a>(
572 &'a self,
573 path: &'a DavPath,
574 options: OpenOptions,
575 mount: &'a Mount,
576 ) -> FsFuture<'a, Box<dyn DavFile>> {
577 Box::pin(async move {
578 let (root, rel) = item(path, mount)?;
579 if options.write || options.append || options.truncate || options.create {
580 writable(&root)?;
581 }
582 let creating = options.create || options.create_new;
583 let full = self
584 .resolve(&root, rel, move |server_root, root_rel, rel| {
585 use crate::fs::FsError as E;
586 // Strict first, so a write lands on the file the path
587 // really names.
588 match crate::fs::resolve_path(server_root, root_rel, rel) {
589 Err(E::NotFound) if creating => {
590 let p = crate::fs::resolve_entry(server_root, root_rel, rel)?;
591 // Nothing resolved, yet the name is taken: a
592 // dangling symlink. Opening that with `create`
593 // would write wherever it points, which may be
594 // outside the root.
595 if std::fs::symlink_metadata(&p).is_ok() {
596 return Err(E::Forbidden);
597 }
598 Ok(p)
599 }
600 other => other,
601 }
602 })
603 .await?;
604 // Taken before the open, so the truncate happens under it too,
605 // and held until the `DavFile` is dropped, which is after the last
606 // byte of the body has landed.
607 let writing = options.write || options.append || options.truncate;
608 let _write = match writing {
609 true => Some(write_lock(&full).lock_owned().await),
610 false => None,
611 };
612 let file = tokio::fs::OpenOptions::new()
613 .read(options.read)
614 .write(options.write)
615 .append(options.append)
616 .truncate(options.truncate)
617 .create(options.create)
618 .create_new(options.create_new)
619 .open(&full)
620 .await
621 .map_err(|e| io_error(&e))?;
622 Ok(Box::new(File { file, _write }) as Box<dyn DavFile>)
623 })
624 }
625
626 /// `meta` decides whether a symlink is described as itself or as what it
627 /// points at, and `dav-server` picks it per operation: `Data` for a
628 /// listing, `DataSymlink` for the walk behind a recursive DELETE or COPY.
629 /// Answering both with followed metadata makes a recursive DELETE descend
630 /// into a linked directory and empty it.
631 fn read_dir<'a>(
632 &'a self,
633 path: &'a DavPath,
634 meta: ReadDirMeta,
635 mount: &'a Mount,
636 ) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
637 Box::pin(async move {
638 let listing = matches!(meta, ReadDirMeta::Data);
639 let entries = match target(path, mount)? {
640 Target::Roots => {
641 // That walk deletes the children before it asks to remove
642 // the collection, so refusing the mount point at
643 // `remove_dir` would come after every root was emptied.
644 // Refusing the listing stops it before anything is touched.
645 if !listing {
646 return Err(FsError::Forbidden);
647 }
648 self.root_entries(mount).await
649 }
650 Target::Item { root, rel } => {
651 // Same for a root's own top. It is a mount point, not a
652 // folder inside one. A whole root cannot be deleted, moved
653 // onto, or copied through the mount.
654 if rel.is_empty() && !listing {
655 return Err(FsError::Forbidden);
656 }
657 let full = self.resolve(&root, rel, crate::fs::resolve_path).await?;
658 // No `MAX_LIST_ENTRIES` cap here, on purpose. PROPFIND has
659 // no way to say "this listing was cut", so a sync client
660 // would read a truncated listing as "the rest was deleted"
661 // and mirror that.
662 blocking(move || {
663 let rd = std::fs::read_dir(&full).map_err(|e| io_error(&e))?;
664 Ok(rd
665 .flatten()
666 .filter_map(|e| {
667 // A link out of the root is still listed. It
668 // refuses to open.
669 let meta = match listing {
670 true => match std::fs::metadata(e.path()) {
671 Ok(m) => Meta::of(&m),
672 // No target to stat: a dangling link.
673 Err(_) => Meta::broken_link(
674 &std::fs::symlink_metadata(e.path()).ok()?,
675 ),
676 },
677 false => Meta::of(&std::fs::symlink_metadata(e.path()).ok()?),
678 };
679 Some(Entry {
680 name: e.file_name().to_string_lossy().into_owned().into_bytes(),
681 meta,
682 })
683 })
684 .collect())
685 })
686 .await?
687 }
688 };
689 let stream = futures_util::stream::iter(
690 entries
691 .into_iter()
692 .map(|e| Ok(Box::new(e) as Box<dyn DavDirEntry>)),
693 );
694 Ok(Box::pin(stream) as FsStream<Box<dyn DavDirEntry>>)
695 })
696 }
697
698 fn metadata<'a>(
699 &'a self,
700 path: &'a DavPath,
701 mount: &'a Mount,
702 ) -> FsFuture<'a, Box<dyn DavMetaData>> {
703 Box::pin(async move {
704 let (root, rel) = match target(path, mount)? {
705 Target::Roots => return Ok(Box::new(Meta::synthetic_dir()) as Box<dyn DavMetaData>),
706 Target::Item { root, rel } => (root, rel),
707 };
708 let full = self.resolve(&root, rel, crate::fs::resolve_path).await?;
709 let meta = blocking(move || std::fs::metadata(&full).map_err(|e| io_error(&e))).await?;
710 Ok(Box::new(Meta::of(&meta)) as Box<dyn DavMetaData>)
711 })
712 }
713
714 /// Metadata of the entry itself. `dav-server` asks this before a DELETE,
715 /// a MOVE, and before overwriting a destination, precisely so it can act
716 /// on a link rather than on what it names.
717 fn symlink_metadata<'a>(
718 &'a self,
719 path: &'a DavPath,
720 mount: &'a Mount,
721 ) -> FsFuture<'a, Box<dyn DavMetaData>> {
722 Box::pin(async move {
723 let (root, rel) = match target(path, mount)? {
724 Target::Roots => return Ok(Box::new(Meta::synthetic_dir()) as Box<dyn DavMetaData>),
725 Target::Item { root, rel } => (root, rel),
726 };
727 let full = self.resolve(&root, rel, crate::fs::resolve_entry).await?;
728 let meta = blocking(move || std::fs::symlink_metadata(&full).map_err(|e| io_error(&e)))
729 .await?;
730 Ok(Box::new(Meta::of(&meta)) as Box<dyn DavMetaData>)
731 })
732 }
733
734 fn create_dir<'a>(&'a self, path: &'a DavPath, mount: &'a Mount) -> FsFuture<'a, ()> {
735 Box::pin(async move {
736 let (root, rel) = item(path, mount)?;
737 writable(&root)?;
738 let (server_root, root_rel) = (self.state.root.clone(), root.path.clone());
739 blocking(move || crate::fs::mkdir(&server_root, &root_rel, &rel).map_err(fs_error))
740 .await
741 })
742 }
743
744 /// Only ever called on an empty directory: `dav-server` walks a tree
745 /// itself and removes the children first.
746 fn remove_dir<'a>(&'a self, path: &'a DavPath, mount: &'a Mount) -> FsFuture<'a, ()> {
747 Box::pin(async move {
748 let (root, rel) = item(path, mount)?;
749 writable(&root)?;
750 let full = self.resolve(&root, rel, crate::fs::resolve_entry).await?;
751 blocking(move || {
752 // A symlink to a directory is listed as a collection, so this
753 // is where DELETE lands on one. Unlink it rather than letting
754 // `remove_dir` fail on a path that is not a directory.
755 let meta = std::fs::symlink_metadata(&full).map_err(|e| io_error(&e))?;
756 match meta.file_type().is_symlink() {
757 true => std::fs::remove_file(&full),
758 false => std::fs::remove_dir(&full),
759 }
760 .map_err(|e| io_error(&e))
761 })
762 .await
763 })
764 }
765
766 fn remove_file<'a>(&'a self, path: &'a DavPath, mount: &'a Mount) -> FsFuture<'a, ()> {
767 Box::pin(async move {
768 let (root, rel) = item(path, mount)?;
769 writable(&root)?;
770 // Not followed: deleting a symlink removes the link, not the file
771 // it names.
772 let full = self.resolve(&root, rel, crate::fs::resolve_entry).await?;
773 blocking(move || std::fs::remove_file(&full).map_err(|e| io_error(&e))).await
774 })
775 }
776
777 fn rename<'a>(
778 &'a self,
779 from: &'a DavPath,
780 to: &'a DavPath,
781 mount: &'a Mount,
782 ) -> FsFuture<'a, ()> {
783 Box::pin(async move {
784 let (src, dst) = (item(from, mount)?, item(to, mount)?);
785 // A move takes the item out of the source root, so that root has
786 // to be writable too.
787 writable(&src.0)?;
788 writable(&dst.0)?;
789 let server_root = self.state.root.clone();
790 blocking(move || {
791 crate::fs::move_to(&server_root, &src.0.path, &src.1, &dst.0.path, &dst.1)
792 .map_err(fs_error)
793 })
794 .await
795 })
796 }
797
798 /// Files only: `dav-server` walks a directory tree itself.
799 fn copy<'a>(
800 &'a self,
801 from: &'a DavPath,
802 to: &'a DavPath,
803 mount: &'a Mount,
804 ) -> FsFuture<'a, ()> {
805 Box::pin(async move {
806 let (src, dst) = (item(from, mount)?, item(to, mount)?);
807 // Only the destination is written. Copying *out of* a read-only
808 // root is fine, and is how a user gets a read-only folder's
809 // contents into a writable one.
810 writable(&dst.0)?;
811 // The same mutex a PUT to this path would take, or a COPY and a
812 // PUT racing for it interleave. Both resolve to the canonical
813 // parent plus the name, so the keys agree.
814 let full = self
815 .resolve(&dst.0, dst.1.clone(), crate::fs::resolve_entry)
816 .await?;
817 let _write = write_lock(&full).lock_owned().await;
818 let server_root = self.state.root.clone();
819 blocking(move || {
820 crate::fs::copy_file_to(&server_root, &src.0.path, &src.1, &dst.0.path, &dst.1)
821 .map_err(fs_error)
822 })
823 .await
824 })
825 }
826}
827
828impl FbFs {
829 /// Run one of the [`crate::fs`] resolvers on the blocking pool.
830 async fn resolve(
831 &self,
832 root: &RootRow,
833 rel: String,
834 f: impl FnOnce(&std::path::Path, &str, &str) -> Result<PathBuf, crate::fs::FsError>
835 + Send
836 + 'static,
837 ) -> FsResult<PathBuf> {
838 let (server_root, root_rel) = (self.state.root.clone(), root.path.clone());
839 blocking(move || f(&server_root, &root_rel, &rel).map_err(fs_error)).await
840 }
841
842 /// The synthetic top-level listing: one entry per mounted root.
843 async fn root_entries(&self, mount: &Mount) -> Vec<Entry> {
844 let mut out = Vec::with_capacity(mount.roots.len());
845 for (seg, root) in mount.roots.iter() {
846 // A root that no longer resolves is skipped rather than reported
847 // as broken: the JSON API hides it the same way.
848 let (server_root, root_rel) = (self.state.root.clone(), root.path.clone());
849 let Ok(full) = blocking(move || {
850 crate::fs::resolve_root(&server_root, &root_rel).map_err(fs_error)
851 })
852 .await
853 else {
854 continue;
855 };
856 let meta = tokio::fs::metadata(&full)
857 .await
858 .map(|m| Meta::of(&m))
859 .unwrap_or_else(|_| Meta::synthetic_dir());
860 out.push(Entry {
861 name: seg.clone().into_bytes(),
862 meta,
863 });
864 }
865 out
866 }
867}
868
869// ---------------------------------------------------------------------------
870// Filesystem value types
871// ---------------------------------------------------------------------------
872
873#[derive(Debug, Clone)]
874struct Meta {
875 len: u64,
876 modified: SystemTime,
877 is_dir: bool,
878 is_symlink: bool,
879}
880
881impl Meta {
882 fn of(m: &std::fs::Metadata) -> Self {
883 Meta {
884 len: m.len(),
885 modified: m.modified().unwrap_or(UNIX_EPOCH),
886 is_dir: m.is_dir(),
887 is_symlink: m.file_type().is_symlink(),
888 }
889 }
890
891 /// A listing entry whose target could not be stat'd: a dangling symlink.
892 ///
893 /// Described as an empty file, not as a link: `dav-server` drops any entry
894 /// a listing reports as a symlink, and a sync client reads a file missing
895 /// from PROPFIND as a deletion to mirror.
896 fn broken_link(m: &std::fs::Metadata) -> Self {
897 Meta {
898 len: 0,
899 is_dir: false,
900 is_symlink: false,
901 ..Meta::of(m)
902 }
903 }
904
905 /// The mount point itself, which is not a directory on disk.
906 fn synthetic_dir() -> Self {
907 Meta {
908 len: 0,
909 modified: UNIX_EPOCH,
910 is_dir: true,
911 is_symlink: false,
912 }
913 }
914}
915
916impl DavMetaData for Meta {
917 fn len(&self) -> u64 {
918 self.len
919 }
920
921 fn modified(&self) -> FsResult<SystemTime> {
922 Ok(self.modified)
923 }
924
925 fn is_dir(&self) -> bool {
926 self.is_dir
927 }
928
929 fn is_symlink(&self) -> bool {
930 self.is_symlink
931 }
932}
933
934#[derive(Debug)]
935struct Entry {
936 name: Vec<u8>,
937 meta: Meta,
938}
939
940impl DavDirEntry for Entry {
941 fn name(&self) -> Vec<u8> {
942 self.name.clone()
943 }
944
945 fn metadata(&self) -> FsFuture<'_, Box<dyn DavMetaData>> {
946 let meta = self.meta.clone();
947 Box::pin(std::future::ready(Ok(
948 Box::new(meta) as Box<dyn DavMetaData>
949 )))
950 }
951}
952
953/// An open file. Plain async I/O: the path was already resolved and checked,
954/// so nothing here needs the blocking pool.
955#[derive(Debug)]
956struct File {
957 file: tokio::fs::File,
958 /// Held for the life of a writable handle. See [`WRITE_LOCKS`].
959 _write: Option<tokio::sync::OwnedMutexGuard<()>>,
960}
961
962/// Ceiling on one `read_bytes` allocation. `dav-server` asks for its own read
963/// buffer size, but the count reaches us from the request, and a short read is
964/// always a valid answer.
965const MAX_READ: usize = 64 * 1024;
966
967impl DavFile for File {
968 fn metadata(&mut self) -> FsFuture<'_, Box<dyn DavMetaData>> {
969 Box::pin(async move {
970 let m = self.file.metadata().await.map_err(|e| io_error(&e))?;
971 Ok(Box::new(Meta::of(&m)) as Box<dyn DavMetaData>)
972 })
973 }
974
975 fn write_buf(&mut self, mut buf: Box<dyn Buf + Send>) -> FsFuture<'_, ()> {
976 Box::pin(async move {
977 while buf.has_remaining() {
978 let n = self
979 .file
980 .write(buf.chunk())
981 .await
982 .map_err(|e| io_error(&e))?;
983 buf.advance(n);
984 }
985 Ok(())
986 })
987 }
988
989 fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
990 Box::pin(async move { self.file.write_all(&buf).await.map_err(|e| io_error(&e)) })
991 }
992
993 fn read_bytes(&mut self, count: usize) -> FsFuture<'_, Bytes> {
994 Box::pin(async move {
995 let mut b = vec![0u8; count.min(MAX_READ)];
996 let n = self.file.read(&mut b).await.map_err(|e| io_error(&e))?;
997 b.truncate(n);
998 Ok(Bytes::from(b))
999 })
1000 }
1001
1002 fn seek(&mut self, pos: SeekFrom) -> FsFuture<'_, u64> {
1003 Box::pin(async move { self.file.seek(pos).await.map_err(|e| io_error(&e)) })
1004 }
1005
1006 fn flush(&mut self) -> FsFuture<'_, ()> {
1007 Box::pin(async move { self.file.flush().await.map_err(|e| io_error(&e)) })
1008 }
1009}
1010
1011// ---------------------------------------------------------------------------
1012// Errors and blocking work
1013// ---------------------------------------------------------------------------
1014
1015/// Run blocking filesystem work, mapping a panic or a shut-down runtime onto
1016/// a 500.
1017async fn blocking<T: Send + 'static>(
1018 f: impl FnOnce() -> FsResult<T> + Send + 'static,
1019) -> FsResult<T> {
1020 tokio::task::spawn_blocking(f)
1021 .await
1022 .map_err(|_| FsError::GeneralFailure)?
1023}
1024
1025fn fs_error(e: crate::fs::FsError) -> FsError {
1026 use crate::fs::FsError as E;
1027 match e {
1028 E::NotFound | E::RootMissing => FsError::NotFound,
1029 E::Conflict => FsError::Exists,
1030 // `Invalid` is a rejected name, which is a refusal, not a 400 here:
1031 // WebDAV has no status for "that name is not allowed".
1032 E::NotADirectory | E::Forbidden | E::Invalid(_) => FsError::Forbidden,
1033 }
1034}
1035
1036/// `dav-server` only derives this from `std::io::Error` when its own `localfs`
1037/// backend is compiled in, which it is not.
1038fn io_error(e: &std::io::Error) -> FsError {
1039 use std::io::ErrorKind as K;
1040 match e.kind() {
1041 K::NotFound => FsError::NotFound,
1042 K::PermissionDenied => FsError::Forbidden,
1043 K::AlreadyExists => FsError::Exists,
1044 K::CrossesDevices => FsError::IsRemote,
1045 // `read_dir` on a file. A refusal, not a server fault.
1046 K::NotADirectory => FsError::Forbidden,
1047 _ => FsError::GeneralFailure,
1048 }
1049}
1050