Skip to main content

aster_forge_webdav/
axum.rs

1//! Optional Axum transport adapter for the `WebDAV` protocol model.
2
3use axum::{
4    body::{Body, to_bytes},
5    extract::Request,
6    response::{IntoResponse, Response},
7};
8use futures::StreamExt;
9use http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Uri};
10
11use crate::{
12    DavBodyPolicy, DavMethod, DavRequestHead, DavRequestOrigin, DavRequestTarget, DavResponse,
13    DavResponseBody, protocol::DavProtocolError,
14};
15
16pub enum DavPreparedBody {
17    None,
18    Xml(Vec<u8>),
19    Bytes(Vec<u8>),
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
23pub enum DavBodyError {
24    #[error("failed to read WebDAV request body")]
25    ReadFailed,
26    #[error("WebDAV request body is too large")]
27    BodyTooLarge,
28    #[error("WebDAV method does not accept a request body")]
29    BodyNotAllowed,
30}
31
32/// Parses an Axum request into a transport-neutral request head.
33///
34/// # Errors
35///
36/// Returns a protocol error when the request target or headers are invalid.
37pub fn request_head(
38    request: &Request,
39    mount_path: &str,
40) -> Result<Option<DavRequestHead>, DavProtocolError> {
41    let target = request_target(request, mount_path)?;
42    let Some(method) = DavMethod::from_name(request.method().as_str()) else {
43        return Ok(None);
44    };
45    let headers = convert_header_map(request.headers())?;
46    DavRequestHead::parse_known_method(method, &target, &headers).map(Some)
47}
48
49/// Parses and validates the request target.
50///
51/// # Errors
52///
53/// Returns a protocol error when the URI or mount path is invalid.
54pub fn request_target<'a>(
55    request: &Request,
56    mount_path: &'a str,
57) -> Result<DavRequestTarget<'a>, DavProtocolError> {
58    let uri: Uri = request
59        .uri()
60        .to_string()
61        .parse()
62        .map_err(|_| DavProtocolError::bad_request("Invalid request URI"))?;
63    let origin = DavRequestOrigin {
64        scheme: request
65            .uri()
66            .scheme_str()
67            .or_else(|| {
68                request
69                    .headers()
70                    .get("x-forwarded-proto")
71                    .and_then(|v| v.to_str().ok())
72            })
73            .unwrap_or("http")
74            .to_string(),
75        host: request
76            .headers()
77            .get("host")
78            .and_then(|v| v.to_str().ok())
79            .unwrap_or_default()
80            .to_string(),
81    };
82    DavRequestHead::parse_target(&uri, mount_path, &origin)
83}
84
85pub fn into_response(response: DavResponse) -> Response {
86    let mut builder = Response::builder().status(response.status);
87    for (name, value) in &response.headers {
88        builder = builder.header(name, value);
89    }
90    let body =
91        match response.body {
92            DavResponseBody::Empty => Body::empty(),
93            DavResponseBody::Bytes(body) => Body::from(body),
94            DavResponseBody::Stream(stream) => Body::from_stream(stream.map(|item| {
95                item.map_err(|_| std::io::Error::other("WebDAV response stream failed"))
96            })),
97            DavResponseBody::MultiStatus(stream) => Body::from_stream(stream.map(|item| {
98                item.map_err(|_| std::io::Error::other("WebDAV response stream failed"))
99            })),
100        };
101    builder.body(body).unwrap_or_else(|_| {
102        (StatusCode::INTERNAL_SERVER_ERROR, "invalid WebDAV response").into_response()
103    })
104}
105
106#[must_use]
107/// Maps a protocol error into an Axum response.
108pub fn protocol_error_response(error: &DavProtocolError) -> Response {
109    into_response(crate::protocol_error_response(error))
110}
111
112/// Copies headers into the transport-neutral `http` map.
113///
114/// # Errors
115///
116/// Returns a protocol error when a header cannot be represented.
117pub fn convert_header_map(source: &HeaderMap) -> Result<HeaderMap, DavProtocolError> {
118    let mut headers = HeaderMap::with_capacity(source.len());
119    for (name, value) in source {
120        let name = HeaderName::from_bytes(name.as_str().as_bytes())
121            .map_err(|_| DavProtocolError::bad_request("Invalid request header"))?;
122        let value = HeaderValue::from_bytes(value.as_bytes())
123            .map_err(|_| DavProtocolError::bad_request("Invalid request header"))?;
124        headers.append(name, value);
125    }
126    Ok(headers)
127}
128
129/// Collects a request body according to the selected bounded body policy.
130///
131/// # Errors
132///
133/// Returns a body error when reading fails, the limit is exceeded, or a body is forbidden.
134pub async fn prepare_request_body(
135    policy: DavBodyPolicy,
136    request: Request,
137) -> Result<DavPreparedBody, DavBodyError> {
138    match policy {
139        DavBodyPolicy::Empty => {
140            let body = to_bytes(request.into_body(), 1)
141                .await
142                .map_err(|_| DavBodyError::ReadFailed)?;
143            if body.is_empty() {
144                Ok(DavPreparedBody::None)
145            } else {
146                Err(DavBodyError::BodyNotAllowed)
147            }
148        }
149        DavBodyPolicy::BoundedXml { maximum } => to_bytes(request.into_body(), maximum)
150            .await
151            .map(|b| DavPreparedBody::Xml(b.to_vec()))
152            .map_err(|error| {
153                if error.to_string().contains("length limit") {
154                    DavBodyError::BodyTooLarge
155                } else {
156                    DavBodyError::ReadFailed
157                }
158            }),
159        DavBodyPolicy::Bounded { maximum } => to_bytes(request.into_body(), maximum)
160            .await
161            .map(|b| DavPreparedBody::Bytes(b.to_vec()))
162            .map_err(|error| {
163                if error.to_string().contains("length limit") {
164                    DavBodyError::BodyTooLarge
165                } else {
166                    DavBodyError::ReadFailed
167                }
168            }),
169        DavBodyPolicy::Stream | DavBodyPolicy::Unused => Ok(DavPreparedBody::None),
170    }
171}