1use std::time::Duration;
4
5use http::header::HeaderMap;
6use http::uri::Authority;
7use http::{StatusCode, Uri};
8use percent_encoding::percent_decode_str;
9
10use crate::{
11 DavBackendError, DavFileSystem, DavIfResourceState, DavIfStateResolver, DavLockSystem, DavPath,
12 FsError,
13};
14use aster_forge_utils::http_validators;
15use async_trait::async_trait;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Depth {
20 Zero,
22 One,
24 Infinity,
26}
27
28impl Depth {
29 #[must_use]
31 pub fn is_infinity(self) -> bool {
32 matches!(self, Self::Infinity)
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Destination {
39 pub path: DavPath,
41 pub relative: String,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct IfHeader {
48 pub groups: Vec<IfResourceGroup>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct IfResourceGroup {
55 pub tagged_path: Option<String>,
57 pub lists: Vec<IfStateList>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct IfStateList {
64 pub conditions: Vec<IfStateCondition>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum IfStateCondition {
71 Token { value: String, negated: bool },
73 Etag { value: String, negated: bool },
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
79pub enum DavIfEvaluationError {
80 #[error(transparent)]
81 Protocol(#[from] DavProtocolError),
82 #[error(transparent)]
83 Backend(#[from] DavBackendError),
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum DavProtocolErrorKind {
89 BadRequest,
90 PreconditionFailed,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
95#[error("{message}")]
96pub struct DavProtocolError {
97 kind: DavProtocolErrorKind,
98 status: StatusCode,
99 message: &'static str,
100}
101
102impl DavProtocolError {
103 #[must_use]
105 pub fn kind(&self) -> DavProtocolErrorKind {
106 self.kind
107 }
108
109 #[must_use]
111 pub fn status(&self) -> StatusCode {
112 self.status
113 }
114
115 #[must_use]
117 pub fn message(&self) -> &'static str {
118 self.message
119 }
120
121 pub(crate) fn bad_request(message: &'static str) -> Self {
122 Self {
123 kind: DavProtocolErrorKind::BadRequest,
124 status: StatusCode::BAD_REQUEST,
125 message,
126 }
127 }
128
129 fn precondition_failed() -> Self {
130 Self {
131 kind: DavProtocolErrorKind::PreconditionFailed,
132 status: StatusCode::PRECONDITION_FAILED,
133 message: "Precondition failed",
134 }
135 }
136}
137
138pub fn parse_propfind_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
144 match parse_depth_header(headers)? {
145 Some(Depth::Zero) => Ok(Depth::Zero),
146 Some(Depth::One) => Ok(Depth::One),
147 Some(Depth::Infinity) | None => Ok(Depth::Infinity),
148 }
149}
150
151pub fn parse_copy_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
157 match parse_depth_header(headers)? {
158 Some(Depth::Zero) => Ok(Depth::Zero),
159 Some(Depth::Infinity) | None => Ok(Depth::Infinity),
160 Some(Depth::One) => Err(DavProtocolError::bad_request("Invalid Depth header")),
161 }
162}
163
164pub fn parse_move_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
170 Ok(parse_depth_header(headers)?.unwrap_or(Depth::Infinity))
171}
172
173pub fn parse_delete_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
179 Ok(parse_depth_header(headers)?.unwrap_or(Depth::Infinity))
180}
181
182pub fn parse_lock_depth(headers: &HeaderMap) -> Result<Depth, DavProtocolError> {
188 match parse_depth_header(headers)? {
189 None | Some(Depth::Infinity) => Ok(Depth::Infinity),
190 Some(Depth::Zero) => Ok(Depth::Zero),
191 Some(Depth::One) => Err(DavProtocolError::bad_request("Invalid Depth header")),
192 }
193}
194
195fn parse_depth_header(headers: &HeaderMap) -> Result<Option<Depth>, DavProtocolError> {
196 let Some(value) = headers.get("Depth") else {
197 return Ok(None);
198 };
199 let value = value
200 .to_str()
201 .map_err(|_| DavProtocolError::bad_request("Invalid Depth header"))?;
202
203 match value {
204 value if value.eq_ignore_ascii_case("0") => Ok(Some(Depth::Zero)),
205 value if value.eq_ignore_ascii_case("1") => Ok(Some(Depth::One)),
206 value if value.eq_ignore_ascii_case("infinity") => Ok(Some(Depth::Infinity)),
207 _ => Err(DavProtocolError::bad_request("Invalid Depth header")),
208 }
209}
210
211pub fn parse_overwrite(headers: &HeaderMap) -> Result<bool, DavProtocolError> {
217 let Some(value) = headers.get("Overwrite") else {
218 return Ok(true);
219 };
220 let value = value
221 .to_str()
222 .map_err(|_| DavProtocolError::bad_request("Invalid Overwrite header"))?
223 .trim();
224 if value.eq_ignore_ascii_case("T") {
225 Ok(true)
226 } else if value.eq_ignore_ascii_case("F") {
227 Ok(false)
228 } else {
229 Err(DavProtocolError::bad_request("Invalid Overwrite header"))
230 }
231}
232
233pub fn destination_relative_path(
239 headers: &HeaderMap,
240 prefix: &str,
241 request_scheme: &str,
242 request_host: &str,
243) -> Result<Destination, DavProtocolError> {
244 let raw = headers
245 .get("Destination")
246 .ok_or_else(|| DavProtocolError::bad_request("Missing Destination header"))?
247 .to_str()
248 .map_err(|_| DavProtocolError::bad_request("Invalid Destination header"))?
249 .trim();
250 let uri: Uri = raw
251 .parse()
252 .map_err(|_| DavProtocolError::bad_request("Invalid Destination header"))?;
253 match (uri.scheme_str(), uri.authority()) {
254 (Some(scheme), Some(authority)) => {
255 if !origin_authority_matches(scheme, authority, request_scheme, request_host) {
256 return Err(DavProtocolError::bad_request(
257 "Destination must stay on this WebDAV server",
258 ));
259 }
260 }
261 (None, None) => {
262 if !raw.starts_with('/') {
263 return Err(DavProtocolError::bad_request("Invalid Destination header"));
264 }
265 }
266 _ => return Err(DavProtocolError::bad_request("Invalid Destination header")),
267 }
268
269 let path = uri.path();
270 let relative = strip_mount_prefix(path, prefix).ok_or_else(|| {
271 DavProtocolError::bad_request("Destination must stay under WebDAV prefix")
272 })?;
273 let path = DavPath::new(relative)
274 .map_err(|_| DavProtocolError::bad_request("Invalid Destination header"))?;
275 let relative = path.as_str().to_string();
276 Ok(Destination { path, relative })
277}
278
279pub fn parse_if_header(headers: &HeaderMap) -> Result<Option<IfHeader>, DavProtocolError> {
285 let Some(value) = headers.get("If") else {
286 return Ok(None);
287 };
288 let raw = value
289 .to_str()
290 .map_err(|_| DavProtocolError::bad_request("Invalid If header"))?;
291 IfHeaderParser::new(raw).parse().map(Some)
292}
293
294pub async fn enforce_if_header(
303 if_header: Option<&IfHeader>,
304 resolver: &dyn DavIfStateResolver,
305 request_path: &DavPath,
306 prefix: &str,
307 request_scheme: &str,
308 request_host: &str,
309) -> Result<(), DavIfEvaluationError> {
310 let Some(if_header) = if_header else {
311 return Ok(());
312 };
313
314 for group in &if_header.groups {
315 let path = match group.tagged_path.as_deref() {
316 Some(tagged_path) => {
317 tagged_dav_path(prefix, tagged_path, request_scheme, request_host)?
318 }
319 None => Some(request_path.clone()),
320 };
321 let state = match path.as_ref() {
322 Some(path) => resolver.resolve_if_state(path).await?,
323 None => DavIfResourceState::default(),
324 };
325 if group
326 .lists
327 .iter()
328 .any(|list| evaluate_if_state_list(list, &state))
329 {
330 return Ok(());
331 }
332 }
333 Err(DavProtocolError::precondition_failed().into())
334}
335
336pub async fn enforce_if_header_with_backends(
346 if_header: Option<&IfHeader>,
347 filesystem: &dyn DavFileSystem,
348 lock_system: &dyn DavLockSystem,
349 request_path: &DavPath,
350 prefix: &str,
351 request_scheme: &str,
352 request_host: &str,
353) -> Result<(), DavIfEvaluationError> {
354 let resolver = BackendIfStateResolver {
355 filesystem,
356 lock_system,
357 };
358 enforce_if_header(
359 if_header,
360 &resolver,
361 request_path,
362 prefix,
363 request_scheme,
364 request_host,
365 )
366 .await
367}
368
369struct BackendIfStateResolver<'a> {
370 filesystem: &'a dyn DavFileSystem,
371 lock_system: &'a dyn DavLockSystem,
372}
373
374#[async_trait]
375impl DavIfStateResolver for BackendIfStateResolver<'_> {
376 async fn resolve_if_state(
377 &self,
378 path: &DavPath,
379 ) -> Result<DavIfResourceState, DavBackendError> {
380 let etag = match self.filesystem.metadata(path).await {
381 Ok(metadata) => metadata.etag(),
382 Err(FsError::NotFound) => None,
383 Err(error) => return Err(error.into()),
384 };
385 let lock_tokens = self
386 .lock_system
387 .discover(path)
388 .await?
389 .into_iter()
390 .map(|lock| lock.token)
391 .collect();
392 Ok(DavIfResourceState { etag, lock_tokens })
393 }
394}
395
396fn evaluate_if_state_list(list: &IfStateList, state: &DavIfResourceState) -> bool {
397 list.conditions.iter().all(|condition| match condition {
398 IfStateCondition::Token { value, negated } => {
399 state.lock_tokens.iter().any(|token| token == value) ^ *negated
400 }
401 IfStateCondition::Etag { value, negated } => {
402 state.etag.as_deref().is_some_and(|etag| {
403 http_validators::if_none_match_header_matches(value, true, Some(etag))
404 .unwrap_or(false)
405 }) ^ *negated
406 }
407 })
408}
409
410fn tagged_dav_path(
411 prefix: &str,
412 tagged_path: &str,
413 request_scheme: &str,
414 request_host: &str,
415) -> Result<Option<DavPath>, DavProtocolError> {
416 let uri: Uri = tagged_path
417 .parse()
418 .map_err(|_| DavProtocolError::bad_request("Invalid If header"))?;
419 let path = match (uri.scheme_str(), uri.authority()) {
420 (Some(scheme), Some(authority)) => {
421 if !origin_authority_matches(scheme, authority, request_scheme, request_host) {
422 return Ok(None);
423 }
424 uri.path()
425 }
426 (None, None) => uri.path(),
427 _ => return Err(DavProtocolError::bad_request("Invalid If header")),
428 };
429 if !path.starts_with('/') {
430 return Err(DavProtocolError::bad_request("Invalid If header"));
431 }
432 let Some(relative) = strip_mount_prefix(path, prefix) else {
433 return Ok(None);
434 };
435 DavPath::new(relative)
436 .map(Some)
437 .map_err(|_| DavProtocolError::bad_request("Invalid If header"))
438}
439
440#[must_use]
442pub fn submitted_lock_tokens_for_path(
443 headers: &HeaderMap,
444 request_path: &str,
445 request_scheme: &str,
446 request_host: &str,
447) -> Vec<String> {
448 let Some(if_header) = parse_if_header(headers).ok().flatten() else {
449 return Vec::new();
450 };
451 submitted_lock_tokens(&if_header, request_path, request_scheme, request_host)
452}
453
454#[must_use]
456pub fn submitted_lock_tokens(
457 if_header: &IfHeader,
458 request_path: &str,
459 request_scheme: &str,
460 request_host: &str,
461) -> Vec<String> {
462 submitted_lock_tokens_matching_path(if_header, request_path, request_scheme, request_host, true)
463}
464
465pub(crate) fn submitted_mutation_lock_tokens(
466 if_header: &IfHeader,
467 request_path: &str,
468 request_scheme: &str,
469 request_host: &str,
470) -> Vec<String> {
471 submitted_lock_tokens_matching_path(
472 if_header,
473 request_path,
474 request_scheme,
475 request_host,
476 false,
477 )
478}
479
480fn submitted_lock_tokens_matching_path(
481 if_header: &IfHeader,
482 request_path: &str,
483 request_scheme: &str,
484 request_host: &str,
485 include_negated: bool,
486) -> Vec<String> {
487 let mut tokens = Vec::new();
488 for group in &if_header.groups {
489 match group.tagged_path.as_deref() {
490 None => {}
491 Some(tagged_path)
492 if if_tag_matches_path(tagged_path, request_path, request_scheme, request_host) => {
493 }
494 Some(_) => continue,
495 }
496 for list in &group.lists {
497 for condition in &list.conditions {
498 if let IfStateCondition::Token { value, negated } = condition
499 && (include_negated || !negated)
500 {
501 tokens.push(value.clone());
502 }
503 }
504 }
505 }
506 tokens.sort();
507 tokens.dedup();
508 tokens
509}
510
511pub fn parse_lock_timeout(
517 headers: &HeaderMap,
518 maximum: Duration,
519) -> Result<Duration, DavProtocolError> {
520 let Some(value) = headers.get("Timeout") else {
521 return Ok(maximum);
522 };
523 let raw = value
524 .to_str()
525 .map_err(|_| DavProtocolError::bad_request("Invalid Timeout header"))?;
526 for candidate in raw
527 .split(',')
528 .map(str::trim)
529 .filter(|value| !value.is_empty())
530 {
531 if candidate.eq_ignore_ascii_case("Infinite") {
532 return Ok(maximum);
533 }
534 if let Some(seconds) = candidate
535 .strip_prefix("Second-")
536 .and_then(|seconds| seconds.parse::<u64>().ok())
537 {
538 return Ok(Duration::from_secs(seconds).min(maximum));
539 }
540 }
541 Err(DavProtocolError::bad_request("Invalid Timeout header"))
542}
543
544pub fn parse_lock_token_header(headers: &HeaderMap) -> Result<String, DavProtocolError> {
550 let raw = headers
551 .get("Lock-Token")
552 .ok_or_else(|| DavProtocolError::bad_request("Missing Lock-Token header"))?
553 .to_str()
554 .map_err(|_| DavProtocolError::bad_request("Invalid Lock-Token header"))?
555 .trim();
556 let token = raw
557 .strip_prefix('<')
558 .and_then(|value| value.strip_suffix('>'))
559 .filter(|token| !token.is_empty() && !token.contains(['<', '>']))
560 .ok_or_else(|| DavProtocolError::bad_request("Invalid Lock-Token header"))?;
561 Ok(token.to_owned())
562}
563
564fn origin_authority_matches(
565 uri_scheme: &str,
566 uri_authority: &Authority,
567 request_scheme: &str,
568 request_host: &str,
569) -> bool {
570 if !uri_scheme.eq_ignore_ascii_case(request_scheme) {
571 return false;
572 }
573 let Ok(request_authority) = request_host.parse::<Authority>() else {
574 return false;
575 };
576 if uri_authority.as_str().contains('@') || request_authority.as_str().contains('@') {
577 return false;
578 }
579 uri_authority
580 .host()
581 .eq_ignore_ascii_case(request_authority.host())
582 && effective_port(uri_scheme, uri_authority)
583 == effective_port(request_scheme, &request_authority)
584}
585
586fn effective_port(scheme: &str, authority: &Authority) -> Option<u16> {
587 authority.port_u16().or_else(|| {
588 if scheme.eq_ignore_ascii_case("http") {
589 Some(80)
590 } else if scheme.eq_ignore_ascii_case("https") {
591 Some(443)
592 } else {
593 None
594 }
595 })
596}
597
598pub(crate) fn strip_mount_prefix<'a>(path: &'a str, prefix: &str) -> Option<&'a str> {
599 path.strip_prefix(prefix).filter(|_| {
600 prefix == "/"
601 || path == prefix
602 || path
603 .as_bytes()
604 .get(prefix.len())
605 .is_some_and(|byte| *byte == b'/')
606 })
607}
608
609fn normalize_lock_token(value: &str) -> String {
610 value
611 .trim()
612 .trim_matches(|character| character == '<' || character == '>')
613 .to_string()
614}
615
616struct IfHeaderParser<'a> {
617 input: &'a str,
618 position: usize,
619}
620
621impl<'a> IfHeaderParser<'a> {
622 fn new(input: &'a str) -> Self {
623 Self { input, position: 0 }
624 }
625
626 fn parse(&mut self) -> Result<IfHeader, DavProtocolError> {
627 self.skip_linear_whitespace();
628 if self.is_eof() {
629 return Err(DavProtocolError::bad_request("Invalid If header"));
630 }
631
632 let tagged = self.peek_char() == Some('<');
633 let mut groups = Vec::new();
634 if tagged {
635 while !self.is_eof() {
636 let tagged_path = self.parse_angle_value()?;
637 let mut lists = Vec::new();
638 loop {
639 self.skip_linear_whitespace();
640 if self.peek_char() != Some('(') {
641 break;
642 }
643 lists.push(self.parse_state_list()?);
644 }
645 if lists.is_empty() {
646 return Err(DavProtocolError::bad_request("Invalid If header"));
647 }
648 groups.push(IfResourceGroup {
649 tagged_path: Some(tagged_path),
650 lists,
651 });
652 self.skip_linear_whitespace();
653 if self.is_eof() {
654 break;
655 }
656 if self.peek_char() != Some('<') {
657 return Err(DavProtocolError::bad_request("Invalid If header"));
658 }
659 }
660 } else {
661 let mut lists = Vec::new();
662 while !self.is_eof() {
663 lists.push(self.parse_state_list()?);
664 self.skip_linear_whitespace();
665 if self.peek_char() == Some('<') {
666 return Err(DavProtocolError::bad_request("Invalid If header"));
667 }
668 }
669 groups.push(IfResourceGroup {
670 tagged_path: None,
671 lists,
672 });
673 }
674 Ok(IfHeader { groups })
675 }
676
677 fn parse_state_list(&mut self) -> Result<IfStateList, DavProtocolError> {
678 self.expect_char('(')?;
679 let mut conditions = Vec::new();
680 loop {
681 self.skip_linear_whitespace();
682 if self.peek_char() == Some(')') {
683 self.position += 1;
684 break;
685 }
686 if self.is_eof() {
687 return Err(DavProtocolError::bad_request("Invalid If header"));
688 }
689
690 let negated = self.consume_not();
691 self.skip_linear_whitespace();
692 let condition = match self.peek_char() {
693 Some('<') => IfStateCondition::Token {
694 value: normalize_lock_token(&self.parse_angle_value()?),
695 negated,
696 },
697 Some('[') => IfStateCondition::Etag {
698 value: self.parse_bracket_value()?,
699 negated,
700 },
701 _ => return Err(DavProtocolError::bad_request("Invalid If header")),
702 };
703 conditions.push(condition);
704 }
705
706 if conditions.is_empty() {
707 return Err(DavProtocolError::bad_request("Invalid If header"));
708 }
709 Ok(IfStateList { conditions })
710 }
711
712 fn parse_angle_value(&mut self) -> Result<String, DavProtocolError> {
713 self.parse_delimited('<', '>')
714 }
715
716 fn parse_bracket_value(&mut self) -> Result<String, DavProtocolError> {
717 self.parse_delimited('[', ']')
718 }
719
720 fn parse_delimited(
721 &mut self,
722 opening: char,
723 closing: char,
724 ) -> Result<String, DavProtocolError> {
725 self.expect_char(opening)?;
726 let start = self.position;
727 while let Some(character) = self.peek_char() {
728 if character == closing {
729 let value = self.input[start..self.position].trim();
730 self.position += closing.len_utf8();
731 if value.is_empty() {
732 return Err(DavProtocolError::bad_request("Invalid If header"));
733 }
734 return Ok(value.to_string());
735 }
736 self.position += character.len_utf8();
737 }
738 Err(DavProtocolError::bad_request("Invalid If header"))
739 }
740
741 fn consume_not(&mut self) -> bool {
742 let rest = &self.input[self.position..];
743 let Some(candidate) = rest.get(..3) else {
744 return false;
745 };
746 if !candidate.eq_ignore_ascii_case("not") {
747 return false;
748 }
749 let after_not = &rest[3..];
750 if after_not.chars().next().is_some_and(|character| {
751 !character.is_ascii_whitespace() && character != '<' && character != '['
752 }) {
753 return false;
754 }
755 self.position += 3;
756 true
757 }
758
759 fn expect_char(&mut self, expected: char) -> Result<(), DavProtocolError> {
760 if self.peek_char() == Some(expected) {
761 self.position += expected.len_utf8();
762 Ok(())
763 } else {
764 Err(DavProtocolError::bad_request("Invalid If header"))
765 }
766 }
767
768 fn skip_linear_whitespace(&mut self) {
769 while self
770 .peek_char()
771 .is_some_and(|character| matches!(character, ' ' | '\t' | '\r' | '\n'))
772 {
773 self.position += 1;
774 }
775 }
776
777 fn peek_char(&self) -> Option<char> {
778 self.input[self.position..].chars().next()
779 }
780
781 fn is_eof(&self) -> bool {
782 self.position >= self.input.len()
783 }
784}
785
786fn if_tag_matches_path(
787 tagged_path: &str,
788 request_path: &str,
789 request_scheme: &str,
790 request_host: &str,
791) -> bool {
792 if path_equivalent(tagged_path, request_path) {
793 return true;
794 }
795 let Ok(uri) = tagged_path.parse::<Uri>() else {
796 return false;
797 };
798 match (uri.scheme_str(), uri.authority()) {
799 (Some(scheme), Some(authority)) => {
800 origin_authority_matches(scheme, authority, request_scheme, request_host)
801 && path_equivalent(uri.path(), request_path)
802 }
803 (None, None) => path_equivalent(uri.path(), request_path),
804 _ => false,
805 }
806}
807
808fn path_equivalent(left: &str, right: &str) -> bool {
809 if left == right {
810 return true;
811 }
812 let left_decoded = percent_decode_str(left).decode_utf8().ok();
813 let right_decoded = percent_decode_str(right).decode_utf8().ok();
814 match (left_decoded.as_deref(), right_decoded.as_deref()) {
815 (Some(left), Some(right)) => left == right,
816 (Some(left), None) => left == right,
817 (None, Some(right)) => left == right,
818 (None, None) => false,
819 }
820}