actix_xml/
lib.rs

1//! XML extractor for actix-web
2//!
3//! This crate provides struct `Xml` that can be used to extract typed information from request's body.
4//!
5//! Under the hood, [quick-xml](https://github.com/tafia/quick-xml) is used to parse payloads.
6//!
7//! ## Example
8//!
9//! ```rust
10//! use actix_web::{web, App};
11//! use actix_xml::Xml;
12//! use serde::Deserialize;
13//!
14//! #[derive(Deserialize)]
15//! struct Info {
16//!     username: String,
17//! }
18//!
19//! /// deserialize `Info` from request's body
20//! async fn index(info: Xml<Info>) -> String {
21//!     format!("Welcome {}!", info.username)
22//! }
23//!
24//! fn main() {
25//!     let app = App::new().service(
26//!         web::resource("/index.html").route(
27//!             web::post().to(index))
28//!     );
29//! }
30//! ```
31//!
32//! ## Features
33//!
34//! - `encoding`: support non utf-8 payload
35//! - `compress-brotli`(default): enable actix-web `compress-brotli` support
36//! - `compress-gzip`(default): enable actix-web `compress-gzip` support
37//! - `compress-zstd`(default): enable actix-web `compress-zstd` support
38//!
39//! If you've removed one of the `compress-*` feature flag for actix-web, make sure to remove it by setting `default-features=false`, or
40//! it will be re-enabled for actix-web.
41
42use std::future::Future;
43use std::pin::Pin;
44use std::task::{Context, Poll};
45use std::{fmt, ops};
46
47use actix_web::dev;
48use actix_web::http::header;
49use actix_web::web::BytesMut;
50use actix_web::Error as ActixError;
51use actix_web::{FromRequest, HttpRequest};
52use futures::future::{err, Either, LocalBoxFuture, Ready};
53use futures::{FutureExt, StreamExt};
54use serde::de::DeserializeOwned;
55
56pub use crate::config::XmlConfig;
57pub use crate::error::XMLPayloadError;
58
59mod config;
60mod error;
61
62#[cfg(test)]
63mod tests;
64
65/// Xml extractor
66///
67/// Xml can be used to extract typed information from request's body.
68///
69/// [**XmlConfig**](struct.XmlConfig.html) allows to configure extraction
70/// process.
71///
72/// ## Example
73///
74/// ```rust
75/// use actix_web::{web, App};
76/// use actix_xml::Xml;
77/// use serde::Deserialize;
78///
79/// #[derive(Deserialize)]
80/// struct Info {
81///     username: String,
82/// }
83///
84/// /// deserialize `Info` from request's body
85/// async fn index(info: Xml<Info>) -> String {
86///     format!("Welcome {}!", info.username)
87/// }
88///
89/// fn main() {
90///     let app = App::new().service(
91///        web::resource("/index.html").route(
92///            web::post().to(index))
93///     );
94/// }
95/// ```
96pub struct Xml<T>(pub T);
97
98impl<T> Xml<T> {
99    /// Deconstruct to an inner value
100    pub fn into_inner(self) -> T {
101        self.0
102    }
103}
104
105impl<T> ops::Deref for Xml<T> {
106    type Target = T;
107
108    fn deref(&self) -> &T {
109        &self.0
110    }
111}
112
113impl<T> ops::DerefMut for Xml<T> {
114    fn deref_mut(&mut self) -> &mut T {
115        &mut self.0
116    }
117}
118
119impl<T> fmt::Debug for Xml<T>
120where
121    T: fmt::Debug,
122{
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "XML: {:?}", self.0)
125    }
126}
127
128impl<T> fmt::Display for Xml<T>
129where
130    T: fmt::Display,
131{
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        fmt::Display::fmt(&self.0, f)
134    }
135}
136
137impl<T> FromRequest for Xml<T>
138where
139    T: DeserializeOwned + 'static,
140{
141    type Error = ActixError;
142    #[allow(clippy::type_complexity)]
143    type Future =
144        Either<LocalBoxFuture<'static, Result<Self, ActixError>>, Ready<Result<Self, ActixError>>>;
145
146    fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
147        let path = req.path().to_string();
148        let config = XmlConfig::from_req(req);
149
150        if let Err(e) = config.check_content_type(req) {
151            return Either::Right(err(e.into()));
152        }
153
154        Either::Left(
155            XmlBody::new(req, payload)
156                .limit(config.limit)
157                .map(move |res| match res {
158                    Err(e) => {
159                        log::debug!(
160                            "Failed to deserialize XML from payload. \
161                         Request path: {}",
162                            path
163                        );
164
165                        Err(e.into())
166                    }
167                    Ok(data) => Ok(Xml(data)),
168                })
169                .boxed_local(),
170        )
171    }
172}
173
174/// Request's payload xml parser, it resolves to a deserialized `T` value.
175/// This future could be used with `ServiceRequest` and `ServiceFromRequest`.
176///
177/// Returns error:
178///
179/// * content type is not `text/xml` or `application/xml`
180///   (unless specified in [`XmlConfig`](struct.XmlConfig.html))
181/// * content length is greater than 256k
182pub struct XmlBody<U> {
183    limit: usize,
184    length: Option<usize>,
185    #[cfg(feature = "__compress")]
186    stream: Option<dev::Decompress<dev::Payload>>,
187    #[cfg(not(feature = "__compress"))]
188    stream: Option<dev::Payload>,
189    err: Option<XMLPayloadError>,
190    fut: Option<LocalBoxFuture<'static, Result<U, XMLPayloadError>>>,
191}
192
193impl<U> XmlBody<U>
194where
195    U: DeserializeOwned + 'static,
196{
197    /// Create `XmlBody` for request.
198    #[allow(clippy::borrow_interior_mutable_const)]
199    pub fn new(req: &HttpRequest, payload: &mut dev::Payload) -> Self {
200        let len = req
201            .headers()
202            .get(&header::CONTENT_LENGTH)
203            .and_then(|l| l.to_str().ok())
204            .and_then(|s| s.parse::<usize>().ok());
205
206        #[cfg(feature = "__compress")]
207        let payload = dev::Decompress::from_headers(payload.take(), req.headers());
208        #[cfg(not(feature = "__compress"))]
209        let payload = payload.take();
210
211        XmlBody {
212            limit: 262_144,
213            length: len,
214            stream: Some(payload),
215            fut: None,
216            err: None,
217        }
218    }
219
220    /// Change max size of payload. By default max size is 256Kb
221    pub fn limit(mut self, limit: usize) -> Self {
222        self.limit = limit;
223        self
224    }
225}
226
227impl<U> Future for XmlBody<U>
228where
229    U: DeserializeOwned + 'static,
230{
231    type Output = Result<U, XMLPayloadError>;
232
233    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
234        if let Some(ref mut fut) = self.fut {
235            return Pin::new(fut).poll(cx);
236        }
237
238        if let Some(err) = self.err.take() {
239            return Poll::Ready(Err(err));
240        }
241
242        let limit = self.limit;
243        if let Some(len) = self.length.take() {
244            if len > limit {
245                return Poll::Ready(Err(XMLPayloadError::Overflow));
246            }
247        }
248        let mut stream = self.stream.take().unwrap();
249
250        self.fut = Some(
251            async move {
252                let mut body = BytesMut::with_capacity(8192);
253
254                while let Some(item) = stream.next().await {
255                    let chunk = item?;
256                    if (body.len() + chunk.len()) > limit {
257                        return Err(XMLPayloadError::Overflow);
258                    } else {
259                        body.extend_from_slice(&chunk);
260                    }
261                }
262                Ok(quick_xml::de::from_reader(&*body)?)
263            }
264            .boxed_local(),
265        );
266
267        self.poll(cx)
268    }
269}