1use std::time::Duration;
4
5use http::header::{CACHE_CONTROL, CONTENT_TYPE};
6use http::{HeaderMap, HeaderValue, StatusCode};
7
8use crate::DavLockSystem;
9use crate::protocol::submitted_mutation_lock_tokens;
10use crate::response::{no_store_empty_response, xml_request_error_response};
11use crate::{
12 DavBackendError, DavErrorCondition, DavLock, DavLockXml, DavMutationCredentials, DavPath,
13 DavProtocolError, DavRequestHead, DavResponse, DavXmlElement, DavXmlError, IfHeader,
14 dav_error_element, dav_lock_discovery_element, dav_lock_response_element, href_for_dav_path,
15 parse_lock_request, parse_lock_timeout, submitted_lock_tokens,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum DavLockPlan {
21 Acquire {
22 owner: Option<DavXmlElement>,
23 timeout: Duration,
24 shared: bool,
25 deep: bool,
26 },
27 Refresh {
28 token: String,
29 timeout: Duration,
30 },
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
35pub enum DavLockPlanError {
36 #[error(transparent)]
37 Protocol(#[from] DavProtocolError),
38 #[error(transparent)]
39 Xml(#[from] DavXmlError),
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
44pub enum DavLockEnforcementError {
45 #[error(transparent)]
47 Backend(#[from] DavBackendError),
48 #[error("a conflicting lock token was not submitted for `{path:?}`")]
50 Conflict { path: DavPath },
51}
52
53pub async fn enforce_unlocked(
59 lock_system: &dyn DavLockSystem,
60 path: &DavPath,
61 deep: bool,
62 prefix: &str,
63 if_header: Option<&IfHeader>,
64 request_scheme: &str,
65 request_host: &str,
66) -> Result<DavMutationCredentials, DavLockEnforcementError> {
67 let evaluation = evaluate_lock_conflicts(
68 lock_system,
69 path,
70 deep,
71 prefix,
72 if_header,
73 request_scheme,
74 request_host,
75 )
76 .await
77 .map_err(DavLockEnforcementError::from)?;
78 if let Some(lock) = evaluation.unsubmitted.into_iter().next() {
79 return Err(DavLockEnforcementError::Conflict { path: *lock.path });
80 }
81 Ok(evaluation.credentials)
82}
83
84struct DavLockConflictEvaluation {
85 credentials: DavMutationCredentials,
86 unsubmitted: Vec<crate::DavLock>,
87}
88
89async fn evaluate_lock_conflicts(
90 lock_system: &dyn DavLockSystem,
91 path: &DavPath,
92 deep: bool,
93 prefix: &str,
94 if_header: Option<&IfHeader>,
95 request_scheme: &str,
96 request_host: &str,
97) -> Result<DavLockConflictEvaluation, DavBackendError> {
98 let conflicts = lock_system.conflicting_locks(path, deep).await?;
99 let mut credentials = DavMutationCredentials::default();
100 let mut unsubmitted = Vec::new();
101 for (index, lock) in conflicts.iter().enumerate() {
102 if conflicts[..index]
103 .iter()
104 .any(|previous| previous.path == lock.path)
105 {
106 continue;
107 }
108 let lock_href = href_for_dav_path(prefix, &lock.path);
109 let submitted_tokens = if_header.map_or_else(Vec::new, |if_header| {
110 submitted_mutation_lock_tokens(if_header, &lock_href, request_scheme, request_host)
111 });
112 let matching_tokens = conflicts
113 .iter()
114 .filter(|candidate| candidate.path == lock.path)
115 .filter_map(|candidate| {
116 submitted_tokens
117 .iter()
118 .find(|token| *token == &candidate.token)
119 .cloned()
120 })
121 .collect::<Vec<_>>();
122 if matching_tokens.is_empty() {
123 unsubmitted.push(lock.clone());
124 } else {
125 credentials.submitted_lock_tokens.extend(matching_tokens);
126 }
127 }
128 credentials.submitted_lock_tokens.sort();
129 credentials.submitted_lock_tokens.dedup();
130 Ok(DavLockConflictEvaluation {
131 credentials,
132 unsubmitted,
133 })
134}
135
136pub async fn unsubmitted_lock_conflicts(
142 lock_system: &dyn DavLockSystem,
143 path: &DavPath,
144 deep: bool,
145 prefix: &str,
146 if_header: Option<&IfHeader>,
147 request_scheme: &str,
148 request_host: &str,
149) -> Result<Vec<crate::DavLock>, DavBackendError> {
150 Ok(evaluate_lock_conflicts(
151 lock_system,
152 path,
153 deep,
154 prefix,
155 if_header,
156 request_scheme,
157 request_host,
158 )
159 .await?
160 .unsubmitted)
161}
162
163pub async fn enforce_parent_unlocked(
169 lock_system: &dyn DavLockSystem,
170 path: &DavPath,
171 prefix: &str,
172 if_header: Option<&IfHeader>,
173 request_scheme: &str,
174 request_host: &str,
175) -> Result<DavMutationCredentials, DavLockEnforcementError> {
176 let Some(parent_path) = path.parent() else {
177 return Ok(DavMutationCredentials::default());
178 };
179 enforce_unlocked(
180 lock_system,
181 &parent_path,
182 false,
183 prefix,
184 if_header,
185 request_scheme,
186 request_host,
187 )
188 .await
189}
190
191pub fn plan_lock_request(
197 headers: &HeaderMap,
198 body: &[u8],
199 request_head: &DavRequestHead,
200 prefix: &str,
201 maximum_timeout: Duration,
202) -> Result<DavLockPlan, DavLockPlanError> {
203 let timeout = parse_lock_timeout(headers, maximum_timeout)?;
204 if body.is_empty() {
205 let request_href = href_for_dav_path(prefix, &request_head.target);
206 let tokens = request_head
207 .if_header
208 .as_ref()
209 .map_or_else(Vec::new, |if_header| {
210 submitted_lock_tokens(
211 if_header,
212 &request_href,
213 &request_head.origin.scheme,
214 &request_head.origin.host,
215 )
216 });
217 if tokens.len() != 1 {
218 return Err(DavProtocolError::bad_request("Invalid LOCK refresh token").into());
219 }
220 return Ok(DavLockPlan::Refresh {
221 token: tokens[0].clone(),
222 timeout,
223 });
224 }
225
226 let request = parse_lock_request(body)?;
227 let depth = request_head
228 .depth
229 .ok_or_else(|| DavProtocolError::bad_request("LOCK Depth was not parsed"))?;
230 Ok(DavLockPlan::Acquire {
231 owner: request.owner,
232 timeout,
233 shared: request.shared,
234 deep: depth.is_infinity(),
235 })
236}
237
238fn lock_success_response(
239 lock: &DavLock,
240 status: StatusCode,
241 prefix: &str,
242 include_lock_token_header: bool,
243) -> Result<DavResponse, DavXmlError> {
244 let body = dav_lock_response_element(&[DavLockXml {
245 token: lock.token.clone(),
246 owner: lock.owner.as_deref().cloned(),
247 timeout: lock.timeout,
248 shared: lock.shared,
249 deep: lock.deep,
250 root_href: href_for_dav_path(prefix, &lock.path),
251 }])
252 .to_bytes()?;
253 let mut response = DavResponse::bytes(status, body);
254 response.headers.insert(
255 CONTENT_TYPE,
256 HeaderValue::from_static("application/xml; charset=utf-8"),
257 );
258 if include_lock_token_header {
259 let value = HeaderValue::from_str(&format!("<{}>", lock.token))
260 .map_err(|_| DavXmlError::Malformed)?;
261 response.headers.insert("Lock-Token", value);
262 }
263 Ok(response)
264}
265
266pub fn lock_refresh_success_response(
272 lock: &DavLock,
273 prefix: &str,
274) -> Result<DavResponse, DavXmlError> {
275 lock_success_response(lock, StatusCode::OK, prefix, false)
276}
277
278pub fn lock_acquire_success_response(
284 lock: &DavLock,
285 prefix: &str,
286 resource_existed: bool,
287) -> Result<DavResponse, DavXmlError> {
288 lock_success_response(
289 lock,
290 if resource_existed {
291 StatusCode::OK
292 } else {
293 StatusCode::CREATED
294 },
295 prefix,
296 true,
297 )
298}
299
300#[must_use]
302pub fn lock_discovery_element(locks: &[DavLock], prefix: &str) -> DavXmlElement {
303 let locks = locks
304 .iter()
305 .map(|lock| DavLockXml {
306 token: lock.token.clone(),
307 owner: lock.owner.as_deref().cloned(),
308 timeout: lock.timeout,
309 shared: lock.shared,
310 deep: lock.deep,
311 root_href: href_for_dav_path(prefix, &lock.path),
312 })
313 .collect::<Vec<_>>();
314 dav_lock_discovery_element(&locks)
315}
316
317pub fn lock_conflict_response(prefix: &str, path: &DavPath) -> Result<DavResponse, DavXmlError> {
323 lock_condition_response(
324 StatusCode::LOCKED,
325 &DavErrorCondition::LockTokenSubmitted {
326 href: href_for_dav_path(prefix, path),
327 },
328 )
329}
330
331pub fn unlock_token_mismatch_response() -> Result<DavResponse, DavXmlError> {
337 lock_condition_response(
338 StatusCode::CONFLICT,
339 &DavErrorCondition::LockTokenMatchesRequestUri,
340 )
341}
342
343#[must_use]
345pub fn unlock_success_response() -> DavResponse {
346 no_store_empty_response(StatusCode::NO_CONTENT)
347}
348
349#[must_use]
351pub fn lock_limit_response() -> DavResponse {
352 let mut response = DavResponse::bytes(
353 StatusCode::INSUFFICIENT_STORAGE,
354 "WebDAV active lock limit exceeded",
355 );
356 response.headers.insert(
357 CONTENT_TYPE,
358 HeaderValue::from_static("text/plain; charset=utf-8"),
359 );
360 response
361 .headers
362 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
363 response
364}
365
366pub fn lock_xml_error_response(error: DavXmlError) -> Result<DavResponse, DavXmlError> {
372 xml_request_error_response(error, "Invalid LOCK body")
373}
374
375fn lock_condition_response(
376 status: StatusCode,
377 condition: &DavErrorCondition,
378) -> Result<DavResponse, DavXmlError> {
379 let mut response = DavResponse::bytes(status, dav_error_element(condition).to_bytes()?);
380 response.headers.insert(
381 CONTENT_TYPE,
382 HeaderValue::from_static("application/xml; charset=utf-8"),
383 );
384 response
385 .headers
386 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
387 Ok(response)
388}