Skip to main content

rama_http/layer/uri/
data.rs

1//! Serve `data:` request URIs from the URI itself (RFC 2397).
2//!
3//! A client stack layered with [`DataUriLayer`] answers `data:` requests
4//! itself and passes every other scheme to the inner service.
5//!
6//! Place this layer *outside* any
7//! [`FollowRedirectLayer`][crate::layer::follow_redirect::FollowRedirectLayer],
8//! so a remote response cannot redirect into it.
9
10use rama_core::{
11    Layer, Service,
12    error::{BoxError, BoxErrorExt, ErrorContext},
13};
14use rama_net::{Protocol, uri::DataUri};
15use rama_utils::macros::define_inner_service_accessors;
16
17use crate::{
18    Body, Method, Request, Response, StatusCode,
19    headers::{ContentType, HttpResponseBuilderExt as _},
20};
21
22/// Serve `data:` request URIs from the URI itself.
23///
24/// See the [module docs](crate::layer::uri::data) for an example.
25#[derive(Debug, Clone, Default)]
26#[non_exhaustive]
27pub struct DataUriLayer;
28
29impl DataUriLayer {
30    /// Create a new [`DataUriLayer`].
31    #[must_use]
32    pub const fn new() -> Self {
33        Self
34    }
35}
36
37impl<S> Layer<S> for DataUriLayer {
38    type Service = DataUriService<S>;
39
40    fn layer(&self, inner: S) -> Self::Service {
41        DataUriService::new(inner)
42    }
43}
44
45/// Serve `data:` request URIs from the URI itself.
46///
47/// See the [module docs](crate::layer::uri::data) for an example.
48#[derive(Debug, Clone)]
49pub struct DataUriService<S> {
50    inner: S,
51}
52
53impl<S> DataUriService<S> {
54    /// Create a new [`DataUriService`].
55    pub const fn new(inner: S) -> Self {
56        Self { inner }
57    }
58
59    define_inner_service_accessors!();
60}
61
62impl<S, ReqBody> Service<Request<ReqBody>> for DataUriService<S>
63where
64    S: Service<Request<ReqBody>, Output = Response, Error: Into<BoxError>>,
65    ReqBody: Send + 'static,
66{
67    type Output = Response;
68    type Error = BoxError;
69
70    async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
71        if req.uri().scheme() != Some(&Protocol::DATA) {
72            return self.inner.serve(req).await.map_err(Into::into);
73        }
74
75        if !matches!(req.method(), &Method::GET | &Method::HEAD) {
76            return Err(BoxError::from_static_str(
77                "data: URIs support GET and HEAD only",
78            ));
79        }
80
81        let data = DataUri::try_from_uri(req.uri()).context("decode data: uri")?;
82        let media_type = data.media_type().clone();
83
84        let body = if req.method() == Method::HEAD {
85            Body::empty()
86        } else {
87            Body::from(data.into_data())
88        };
89        Response::builder()
90            .status(StatusCode::OK)
91            .typed_header(ContentType::new(media_type))
92            .body(body)
93            .context("build data: response")
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use std::convert::Infallible;
100
101    use rama_core::service::service_fn;
102
103    use super::*;
104    use crate::{BodyExtractExt as _, headers::HeaderMapExt as _};
105    use rama_net::uri::{DEFAULT_DATA_MEDIA_TYPE, Uri};
106
107    fn service() -> DataUriService<impl Service<Request, Output = Response, Error = Infallible>> {
108        DataUriService::new(service_fn(async |_: Request| {
109            Ok::<_, Infallible>(Response::new(Body::from("remote")))
110        }))
111    }
112
113    async fn get(uri: &str) -> Result<Response, BoxError> {
114        let uri: Uri = uri.parse().unwrap();
115        service()
116            .serve(Request::get(uri).body(Body::empty()).unwrap())
117            .await
118    }
119
120    fn content_type(resp: &Response) -> crate::mime::Mime {
121        resp.headers()
122            .typed_get::<ContentType>()
123            .unwrap()
124            .into_mime()
125    }
126
127    #[tokio::test]
128    async fn non_data_scheme_passes_through() {
129        let resp = get("http://example.com/x").await.unwrap();
130        assert_eq!(resp.try_into_string().await.unwrap(), "remote");
131    }
132
133    #[tokio::test]
134    async fn plain_payload() {
135        let resp = get("data:,hello%20world").await.unwrap();
136        assert_eq!(resp.status(), StatusCode::OK);
137        assert_eq!(
138            content_type(&resp),
139            DEFAULT_DATA_MEDIA_TYPE
140                .parse::<crate::mime::Mime>()
141                .unwrap()
142        );
143        assert_eq!(resp.try_into_string().await.unwrap(), "hello world");
144    }
145
146    #[tokio::test]
147    async fn base64_payload_with_media_type() {
148        let resp = get("data:text/javascript;base64,RElSRUNU").await.unwrap();
149        assert_eq!(content_type(&resp), crate::mime::TEXT_JAVASCRIPT);
150        assert_eq!(resp.try_into_string().await.unwrap(), "DIRECT");
151    }
152
153    #[tokio::test]
154    async fn media_type_is_preserved() {
155        let resp = get("data:application/x-ns-proxy-autoconfig,DIRECT")
156            .await
157            .unwrap();
158        assert_eq!(
159            content_type(&resp).as_ref(),
160            "application/x-ns-proxy-autoconfig"
161        );
162    }
163
164    #[tokio::test]
165    async fn payload_keeps_query_and_drops_fragment() {
166        // `?` is part of the payload, not a query: a PAC ternary must survive
167        let resp = get("data:application/x-ns-proxy-autoconfig,return h==host?DIRECT:PROXY")
168            .await
169            .unwrap();
170        assert_eq!(
171            resp.try_into_string().await.unwrap(),
172            "return h==host?DIRECT:PROXY"
173        );
174
175        let resp = get("data:,a?b#c").await.unwrap();
176        assert_eq!(resp.try_into_string().await.unwrap(), "a?b");
177    }
178
179    #[tokio::test]
180    async fn head_has_no_body() {
181        let resp = service()
182            .serve(
183                Request::builder()
184                    .method(Method::HEAD)
185                    .uri("data:,hello")
186                    .body(Body::empty())
187                    .unwrap(),
188            )
189            .await
190            .unwrap();
191        assert_eq!(resp.try_into_string().await.unwrap(), "");
192    }
193
194    #[tokio::test]
195    async fn missing_comma_errors() {
196        let _err = get("data:text/plain").await.unwrap_err();
197    }
198
199    #[tokio::test]
200    async fn invalid_media_type_errors() {
201        let _err = get("data:not a mime,payload").await.unwrap_err();
202    }
203
204    #[tokio::test]
205    async fn invalid_base64_errors() {
206        let _err = get("data:text/plain;base64,!!!not-base64!!!")
207            .await
208            .unwrap_err();
209    }
210
211    #[tokio::test]
212    async fn rejects_non_get_methods() {
213        let err = service()
214            .serve(
215                Request::builder()
216                    .method(Method::POST)
217                    .uri("data:,x")
218                    .body(Body::empty())
219                    .unwrap(),
220            )
221            .await
222            .unwrap_err();
223        assert!(err.to_string().contains("GET and HEAD"), "{err}");
224    }
225}