Skip to main content

aster_forge_webdav/
actix.rs

1//! Optional Actix transport adapter for the WebDAV protocol model.
2
3use actix_web::http::{StatusCode as ActixStatusCode, header as actix_header};
4use actix_web::{HttpRequest, HttpResponse};
5use futures::StreamExt;
6use http::{HeaderMap, HeaderName, HeaderValue, Uri};
7
8use crate::protocol::DavProtocolError;
9use crate::{
10    DavBodyError, DavBodyPolicy, DavFileSystem, DavIfEvaluationError, DavLockSystem, DavMethod,
11    DavPath, DavPrecondition, DavRequestHead, DavRequestOrigin, DavResponse, DavResponseBody,
12    IfHeader,
13};
14
15/// Request body prepared according to the selected WebDAV method contract.
16pub enum DavPreparedBody {
17    None,
18    Xml(Vec<u8>),
19}
20
21impl DavPreparedBody {
22    /// Returns the collected XML bytes, or an empty slice for bodyless methods.
23    #[must_use]
24    pub fn xml(&self) -> &[u8] {
25        match self {
26            Self::None => &[],
27            Self::Xml(body) => body,
28        }
29    }
30}
31
32/// Parses an Actix request into the transport-neutral request head.
33pub fn request_head(
34    request: &HttpRequest,
35    mount_path: &str,
36) -> Result<Option<DavRequestHead>, DavProtocolError> {
37    let Some(method) = DavMethod::from_name(request.method().as_str()) else {
38        return Ok(None);
39    };
40    let uri: Uri = request
41        .uri()
42        .to_string()
43        .parse()
44        .map_err(|_| DavProtocolError::bad_request("Invalid request URI"))?;
45    let headers = convert_header_map(request.headers())?;
46    let connection = request.connection_info();
47    let origin = DavRequestOrigin {
48        scheme: connection.scheme().to_string(),
49        host: connection.host().to_string(),
50    };
51    DavRequestHead::parse(method, &uri, &headers, mount_path, &origin).map(Some)
52}
53
54/// Converts a transport-neutral response into an Actix response.
55pub fn into_response(response: DavResponse) -> HttpResponse {
56    let status = ActixStatusCode::from_u16(response.status.as_u16())
57        .unwrap_or(ActixStatusCode::INTERNAL_SERVER_ERROR);
58    let mut builder = HttpResponse::build(status);
59    for (name, value) in &response.headers {
60        let name = actix_header::HeaderName::from_bytes(name.as_str().as_bytes());
61        let value = actix_header::HeaderValue::from_bytes(value.as_bytes());
62        if let (Ok(name), Ok(value)) = (name, value) {
63            builder.insert_header((name, value));
64        }
65    }
66    match response.body {
67        DavResponseBody::Empty => builder.finish(),
68        DavResponseBody::Bytes(body) => builder.body(body),
69        DavResponseBody::Stream(stream) => {
70            let stream = stream.map(|item| {
71                item.map_err(|error| actix_web::error::ErrorInternalServerError(error.to_string()))
72            });
73            builder.streaming(stream)
74        }
75    }
76}
77
78/// Maps a transport-neutral protocol error into its Actix response.
79#[must_use]
80pub fn protocol_error_response(error: DavProtocolError) -> HttpResponse {
81    into_response(crate::protocol_error_response(&error))
82}
83
84/// Copies Actix headers into the transport-neutral map and maps malformed input to a response.
85pub fn converted_headers(source: &actix_header::HeaderMap) -> Result<HeaderMap, HttpResponse> {
86    convert_header_map(source).map_err(protocol_error_response)
87}
88
89/// Resolves and enforces a parsed WebDAV `If` header through the canonical backend ports.
90pub async fn enforce_if_header_with_backends(
91    if_header: Option<&IfHeader>,
92    filesystem: &dyn DavFileSystem,
93    lock_system: &dyn DavLockSystem,
94    request_path: &DavPath,
95    prefix: &str,
96    request_scheme: &str,
97    request_host: &str,
98) -> Result<(), HttpResponse> {
99    match crate::enforce_if_header_with_backends(
100        if_header,
101        filesystem,
102        lock_system,
103        request_path,
104        prefix,
105        request_scheme,
106        request_host,
107    )
108    .await
109    {
110        Ok(()) => Ok(()),
111        Err(DavIfEvaluationError::Protocol(error)) => Err(protocol_error_response(error)),
112        Err(DavIfEvaluationError::Backend(error)) => {
113            Err(into_response(crate::backend_error_response(&error)))
114        }
115    }
116}
117
118/// Enforces resource lock submission and maps the protocol response to Actix.
119pub async fn enforce_unlocked(
120    lock_system: &dyn DavLockSystem,
121    path: &DavPath,
122    deep: bool,
123    prefix: &str,
124    if_header: Option<&IfHeader>,
125    request_scheme: &str,
126    request_host: &str,
127) -> Result<(), HttpResponse> {
128    crate::enforce_unlocked(
129        lock_system,
130        path,
131        deep,
132        prefix,
133        if_header,
134        request_scheme,
135        request_host,
136    )
137    .await
138    .map_err(into_response)
139}
140
141/// Enforces lock submission for the canonical parent and maps the response to Actix.
142pub async fn enforce_parent_unlocked(
143    lock_system: &dyn DavLockSystem,
144    path: &DavPath,
145    prefix: &str,
146    if_header: Option<&IfHeader>,
147    request_scheme: &str,
148    request_host: &str,
149) -> Result<(), HttpResponse> {
150    crate::enforce_parent_unlocked(
151        lock_system,
152        path,
153        prefix,
154        if_header,
155        request_scheme,
156        request_host,
157    )
158    .await
159    .map_err(into_response)
160}
161
162/// Evaluates HTTP ETag preconditions from Actix headers and maps protocol failures to a response.
163pub fn evaluate_http_etag_preconditions(
164    headers: &actix_header::HeaderMap,
165    resource_exists: bool,
166    current_etag: Option<&str>,
167    safe_method: bool,
168) -> Result<DavPrecondition, HttpResponse> {
169    let headers = converted_headers(headers)?;
170    crate::evaluate_http_etag_preconditions(&headers, resource_exists, current_etag, safe_method)
171        .map_err(protocol_error_response)
172}
173
174/// Copies Actix header types into the transport-neutral `http` 1.x map.
175pub fn convert_header_map(source: &actix_header::HeaderMap) -> Result<HeaderMap, DavProtocolError> {
176    let mut headers = HeaderMap::with_capacity(source.len());
177    for (name, value) in source {
178        let name = HeaderName::from_bytes(name.as_str().as_bytes())
179            .map_err(|_| DavProtocolError::bad_request("Invalid request header"))?;
180        let value = HeaderValue::from_bytes(value.as_bytes())
181            .map_err(|_| DavProtocolError::bad_request("Invalid request header"))?;
182        headers.append(name, value);
183    }
184    Ok(headers)
185}
186
187/// Rejects the first non-empty request body chunk without buffering the remaining payload.
188pub async fn ensure_empty_body(payload: &mut actix_web::web::Payload) -> Result<(), DavBodyError> {
189    while let Some(chunk) = payload.next().await {
190        let chunk = chunk.map_err(|_| DavBodyError::ReadFailed)?;
191        if !chunk.is_empty() {
192            return Err(DavBodyError::BodyNotAllowed);
193        }
194    }
195    Ok(())
196}
197
198/// Collects a bounded XML request body for grammar parsing by the protocol layer.
199pub async fn collect_bounded_xml_body(
200    payload: &mut actix_web::web::Payload,
201    maximum: usize,
202) -> Result<Vec<u8>, DavBodyError> {
203    let mut body = Vec::with_capacity(maximum.min(4096));
204    while let Some(chunk) = payload.next().await {
205        let chunk = chunk.map_err(|_| DavBodyError::ReadFailed)?;
206        let next_len = body
207            .len()
208            .checked_add(chunk.len())
209            .ok_or(DavBodyError::XmlTooLarge)?;
210        if next_len > maximum {
211            return Err(DavBodyError::XmlTooLarge);
212        }
213        body.extend_from_slice(&chunk);
214    }
215    Ok(body)
216}
217
218/// Applies the method-owned body policy while leaving streaming PUT bodies untouched.
219pub async fn prepare_request_body(
220    method: DavMethod,
221    payload: &mut actix_web::web::Payload,
222    xml_limit: usize,
223) -> Result<DavPreparedBody, DavBodyError> {
224    match method.body_policy() {
225        DavBodyPolicy::Empty => ensure_empty_body(payload)
226            .await
227            .map(|()| DavPreparedBody::None),
228        DavBodyPolicy::BoundedXml => collect_bounded_xml_body(payload, xml_limit)
229            .await
230            .map(DavPreparedBody::Xml),
231        DavBodyPolicy::Stream | DavBodyPolicy::Unused => Ok(DavPreparedBody::None),
232    }
233}