1use std::time::SystemTime;
4
5use aster_forge_utils::http_range::{HttpByteRange, HttpRangeError, parse_single_byte_range};
6use aster_forge_utils::http_validators::{
7 format_http_date, http_date_epoch_seconds, parse_http_date,
8};
9use bytes::Bytes;
10use http::header::{
11 ACCEPT_RANGES, ALLOW, CACHE_CONTROL, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE,
12 CONTENT_TYPE, ETAG, IF_RANGE, LAST_MODIFIED, RANGE,
13};
14use http::{HeaderMap, HeaderValue, StatusCode};
15
16use crate::{
17 DavBackendError, DavBackendErrorKind, DavContentStream, DavErrorCondition, DavPrecondition,
18 DavProtocolError, DavProtocolErrorKind, DavXmlElement, DavXmlError, dav_error_element,
19};
20
21pub const DAV_ALLOW_HEADER: &str = "OPTIONS, GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, VERSION-CONTROL";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
26pub enum DavBodyError {
27 #[error("failed to read WebDAV request body")]
28 ReadFailed,
29 #[error("WebDAV XML body is too large")]
30 XmlTooLarge,
31 #[error("WebDAV method does not accept a request body")]
32 BodyNotAllowed,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DavDownloadBody {
38 Empty,
40 Full,
42 Range(HttpByteRange),
44}
45
46pub struct DavDownloadPlan {
48 pub response: DavResponse,
49 pub body: DavDownloadBody,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
54pub enum DavDownloadPlanError {
55 #[error(transparent)]
56 Protocol(#[from] DavProtocolError),
57 #[error("invalid WebDAV download representation metadata")]
58 InvalidRepresentation,
59}
60
61pub enum DavResponseBody {
63 Empty,
64 Bytes(Bytes),
65 Stream(DavContentStream),
66}
67
68pub struct DavResponse {
70 pub status: StatusCode,
71 pub headers: HeaderMap,
72 pub body: DavResponseBody,
73}
74
75impl DavResponse {
76 #[must_use]
78 pub fn empty(status: StatusCode) -> Self {
79 Self {
80 status,
81 headers: HeaderMap::new(),
82 body: DavResponseBody::Empty,
83 }
84 }
85
86 #[must_use]
88 pub fn bytes(status: StatusCode, body: impl Into<Bytes>) -> Self {
89 Self {
90 status,
91 headers: HeaderMap::new(),
92 body: DavResponseBody::Bytes(body.into()),
93 }
94 }
95}
96
97pub(crate) fn xml_document_response(
98 status: StatusCode,
99 root: DavXmlElement,
100) -> Result<DavResponse, DavXmlError> {
101 let mut response = DavResponse::bytes(status, root.to_bytes()?);
102 response.headers.insert(
103 CONTENT_TYPE,
104 HeaderValue::from_static("application/xml; charset=utf-8"),
105 );
106 if status.is_client_error() || status.is_server_error() {
107 response
108 .headers
109 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
110 }
111 Ok(response)
112}
113
114pub(crate) fn text_document_response(status: StatusCode, body: impl Into<String>) -> DavResponse {
115 let mut response = DavResponse::bytes(status, body.into());
116 response.headers.insert(
117 CONTENT_TYPE,
118 HeaderValue::from_static("text/plain; charset=utf-8"),
119 );
120 if status.is_client_error() || status.is_server_error() {
121 response
122 .headers
123 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
124 }
125 response
126}
127
128pub(crate) fn xml_request_error_response(
129 error: DavXmlError,
130 invalid_grammar_message: &'static str,
131) -> Result<DavResponse, DavXmlError> {
132 match error {
133 DavXmlError::ExternalEntity => xml_document_response(
134 StatusCode::FORBIDDEN,
135 dav_error_element(&DavErrorCondition::NoExternalEntities),
136 ),
137 DavXmlError::TooLarge => Ok(text_document_response(
138 StatusCode::PAYLOAD_TOO_LARGE,
139 "WebDAV XML body too large",
140 )),
141 DavXmlError::TooDeep | DavXmlError::Malformed => Ok(text_document_response(
142 StatusCode::BAD_REQUEST,
143 "Invalid XML body",
144 )),
145 DavXmlError::InvalidGrammar => Ok(text_document_response(
146 StatusCode::BAD_REQUEST,
147 invalid_grammar_message,
148 )),
149 }
150}
151
152#[must_use]
154pub fn protocol_error_response(error: &DavProtocolError) -> DavResponse {
155 match error.kind() {
156 DavProtocolErrorKind::BadRequest => text_document_response(error.status(), error.message()),
157 DavProtocolErrorKind::PreconditionFailed => no_store_empty_response(error.status()),
158 }
159}
160
161#[must_use]
163pub fn backend_error_response(error: &DavBackendError) -> DavResponse {
164 let status = match error.kind {
165 DavBackendErrorKind::NotFound => StatusCode::NOT_FOUND,
166 DavBackendErrorKind::Forbidden => StatusCode::FORBIDDEN,
167 DavBackendErrorKind::Conflict | DavBackendErrorKind::AlreadyExists => StatusCode::CONFLICT,
168 DavBackendErrorKind::InsufficientStorage => StatusCode::INSUFFICIENT_STORAGE,
169 DavBackendErrorKind::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
170 DavBackendErrorKind::Locked => StatusCode::LOCKED,
171 DavBackendErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
172 DavBackendErrorKind::Unsupported => StatusCode::METHOD_NOT_ALLOWED,
173 DavBackendErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
174 };
175 no_store_empty_response(status)
176}
177
178pub(crate) fn no_store_empty_response(status: StatusCode) -> DavResponse {
179 let mut response = DavResponse::empty(status);
180 response
181 .headers
182 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
183 response
184}
185
186#[must_use]
188pub fn options_response() -> DavResponse {
189 let mut response = DavResponse::empty(StatusCode::OK);
190 response
191 .headers
192 .insert(ALLOW, HeaderValue::from_static(DAV_ALLOW_HEADER));
193 response
194 .headers
195 .insert("DAV", HeaderValue::from_static("1, 2, version-control"));
196 response
197 .headers
198 .insert("MS-Author-Via", HeaderValue::from_static("DAV"));
199 response
200}
201
202#[must_use]
204pub fn method_not_allowed_response() -> DavResponse {
205 let mut response = DavResponse::empty(StatusCode::METHOD_NOT_ALLOWED);
206 response
207 .headers
208 .insert(ALLOW, HeaderValue::from_static(DAV_ALLOW_HEADER));
209 response
210 .headers
211 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
212 response
213}
214
215#[must_use]
217pub fn body_error_response(error: DavBodyError) -> DavResponse {
218 let (status, body) = match error {
219 DavBodyError::ReadFailed => (StatusCode::BAD_REQUEST, Some("Failed to read request body")),
220 DavBodyError::XmlTooLarge => (
221 StatusCode::PAYLOAD_TOO_LARGE,
222 Some("WebDAV XML body too large"),
223 ),
224 DavBodyError::BodyNotAllowed => (StatusCode::UNSUPPORTED_MEDIA_TYPE, None),
225 };
226 let mut response = match body {
227 Some(body) => DavResponse::bytes(status, body),
228 None => DavResponse::empty(status),
229 };
230 response
231 .headers
232 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
233 if body.is_some() {
234 response.headers.insert(
235 CONTENT_TYPE,
236 HeaderValue::from_static("text/plain; charset=utf-8"),
237 );
238 }
239 response
240}
241
242pub fn plan_download_response(
244 headers: &HeaderMap,
245 head_only: bool,
246 content_length: u64,
247 content_type: &str,
248 etag: Option<&str>,
249 last_modified: SystemTime,
250) -> Result<DavDownloadPlan, DavDownloadPlanError> {
251 match crate::evaluate_http_download_preconditions(headers, etag, Some(last_modified))? {
252 DavPrecondition::Proceed => {}
253 DavPrecondition::NotModified => {
254 let mut response = DavResponse::empty(StatusCode::NOT_MODIFIED);
255 insert_validators(&mut response.headers, etag, last_modified)?;
256 return Ok(DavDownloadPlan {
257 response,
258 body: DavDownloadBody::Empty,
259 });
260 }
261 }
262
263 let range = if head_only || !if_range_matches(headers, etag, last_modified) {
264 None
265 } else if let Some(value) = headers.get(RANGE) {
266 let Ok(raw) = value.to_str() else {
267 return Ok(range_not_satisfiable_plan(content_length));
268 };
269 match parse_single_byte_range(raw, content_length) {
270 Ok(range) => Some(range),
271 Err(HttpRangeError::UnsupportedUnit | HttpRangeError::MultipleRangesUnsupported) => {
272 None
273 }
274 Err(
275 HttpRangeError::Malformed
276 | HttpRangeError::InvalidNumber
277 | HttpRangeError::EmptyRepresentation
278 | HttpRangeError::Unsatisfiable,
279 ) => return Ok(range_not_satisfiable_plan(content_length)),
280 }
281 } else {
282 None
283 };
284
285 let (status, response_length, body) = match range {
286 Some(range) => (
287 StatusCode::PARTIAL_CONTENT,
288 range.length(),
289 DavDownloadBody::Range(range),
290 ),
291 None if head_only => (StatusCode::OK, content_length, DavDownloadBody::Empty),
292 None => (StatusCode::OK, content_length, DavDownloadBody::Full),
293 };
294 let mut response = DavResponse::empty(status);
295 response
296 .headers
297 .insert(CONTENT_LENGTH, header_value(&response_length.to_string())?);
298 response
299 .headers
300 .insert(CONTENT_TYPE, header_value(content_type)?);
301 response
302 .headers
303 .insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
304 response
305 .headers
306 .insert(CONTENT_ENCODING, HeaderValue::from_static("identity"));
307 if let Some(range) = range {
308 response
309 .headers
310 .insert(CONTENT_RANGE, header_value(&range.content_range_header())?);
311 }
312 insert_validators(&mut response.headers, etag, last_modified)?;
313
314 Ok(DavDownloadPlan { response, body })
315}
316
317fn if_range_matches(headers: &HeaderMap, etag: Option<&str>, last_modified: SystemTime) -> bool {
318 let Some(value) = headers.get(IF_RANGE) else {
319 return true;
320 };
321 let Ok(raw) = value.to_str() else {
322 return false;
323 };
324 let raw = raw.trim();
325 if raw.starts_with('"')
326 || raw
327 .get(..2)
328 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("W/"))
329 {
330 return strong_if_range_etag_matches(raw, etag);
331 }
332 parse_http_date(raw)
333 .is_ok_and(|date| http_date_epoch_seconds(date) == http_date_epoch_seconds(last_modified))
334}
335
336fn strong_if_range_etag_matches(candidate: &str, current: Option<&str>) -> bool {
337 if candidate
338 .get(..2)
339 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("W/"))
340 {
341 return false;
342 }
343 let Some(candidate) = candidate
344 .strip_prefix('"')
345 .and_then(|value| value.strip_suffix('"'))
346 .filter(|value| !value.contains('"'))
347 else {
348 return false;
349 };
350 let Some(current) = current else {
351 return false;
352 };
353 if current
354 .trim()
355 .get(..2)
356 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("W/"))
357 {
358 return false;
359 }
360 let current = current.trim();
361 let current = current
362 .strip_prefix('"')
363 .and_then(|value| value.strip_suffix('"'))
364 .unwrap_or(current);
365 candidate == current
366}
367
368#[must_use]
370pub fn range_not_satisfiable_response(content_length: u64) -> DavResponse {
371 let mut response = DavResponse::empty(StatusCode::RANGE_NOT_SATISFIABLE);
372 response.headers.insert(
373 CONTENT_RANGE,
374 HeaderValue::from_str(&format!("bytes */{content_length}"))
375 .unwrap_or_else(|_| HeaderValue::from_static("bytes */0")),
376 );
377 response
378 .headers
379 .insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
380 response
381 .headers
382 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
383 response
384}
385
386fn range_not_satisfiable_plan(content_length: u64) -> DavDownloadPlan {
387 DavDownloadPlan {
388 response: range_not_satisfiable_response(content_length),
389 body: DavDownloadBody::Empty,
390 }
391}
392
393fn insert_validators(
394 headers: &mut HeaderMap,
395 etag: Option<&str>,
396 last_modified: SystemTime,
397) -> Result<(), DavDownloadPlanError> {
398 headers.insert(
399 LAST_MODIFIED,
400 header_value(&format_http_date(last_modified))?,
401 );
402 if let Some(etag) = etag {
403 headers.insert(ETAG, header_value(&format!("\"{etag}\""))?);
404 }
405 Ok(())
406}
407
408fn header_value(value: &str) -> Result<HeaderValue, DavDownloadPlanError> {
409 HeaderValue::from_str(value).map_err(|_| DavDownloadPlanError::InvalidRepresentation)
410}