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