use rama_core::{
Layer, Service,
error::{BoxError, BoxErrorExt, ErrorContext},
};
use rama_net::{Protocol, uri::DataUri};
use rama_utils::macros::define_inner_service_accessors;
use crate::{
Body, Method, Request, Response, StatusCode,
headers::{ContentType, HttpResponseBuilderExt as _},
};
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct DataUriLayer;
impl DataUriLayer {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl<S> Layer<S> for DataUriLayer {
type Service = DataUriService<S>;
fn layer(&self, inner: S) -> Self::Service {
DataUriService::new(inner)
}
}
#[derive(Debug, Clone)]
pub struct DataUriService<S> {
inner: S,
}
impl<S> DataUriService<S> {
pub const fn new(inner: S) -> Self {
Self { inner }
}
define_inner_service_accessors!();
}
impl<S, ReqBody> Service<Request<ReqBody>> for DataUriService<S>
where
S: Service<Request<ReqBody>, Output = Response, Error: Into<BoxError>>,
ReqBody: Send + 'static,
{
type Output = Response;
type Error = BoxError;
async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
if req.uri().scheme() != Some(&Protocol::DATA) {
return self.inner.serve(req).await.map_err(Into::into);
}
if !matches!(req.method(), &Method::GET | &Method::HEAD) {
return Err(BoxError::from_static_str(
"data: URIs support GET and HEAD only",
));
}
let data = DataUri::try_from_uri(req.uri()).context("decode data: uri")?;
let media_type = data.media_type().clone();
let body = if req.method() == Method::HEAD {
Body::empty()
} else {
Body::from(data.into_data())
};
Response::builder()
.status(StatusCode::OK)
.typed_header(ContentType::new(media_type))
.body(body)
.context("build data: response")
}
}
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use rama_core::service::service_fn;
use super::*;
use crate::{BodyExtractExt as _, headers::HeaderMapExt as _};
use rama_net::uri::{DEFAULT_DATA_MEDIA_TYPE, Uri};
fn service() -> DataUriService<impl Service<Request, Output = Response, Error = Infallible>> {
DataUriService::new(service_fn(async |_: Request| {
Ok::<_, Infallible>(Response::new(Body::from("remote")))
}))
}
async fn get(uri: &str) -> Result<Response, BoxError> {
let uri: Uri = uri.parse().unwrap();
service()
.serve(Request::get(uri).body(Body::empty()).unwrap())
.await
}
fn content_type(resp: &Response) -> crate::mime::Mime {
resp.headers()
.typed_get::<ContentType>()
.unwrap()
.into_mime()
}
#[tokio::test]
async fn non_data_scheme_passes_through() {
let resp = get("http://example.com/x").await.unwrap();
assert_eq!(resp.try_into_string().await.unwrap(), "remote");
}
#[tokio::test]
async fn plain_payload() {
let resp = get("data:,hello%20world").await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
content_type(&resp),
DEFAULT_DATA_MEDIA_TYPE
.parse::<crate::mime::Mime>()
.unwrap()
);
assert_eq!(resp.try_into_string().await.unwrap(), "hello world");
}
#[tokio::test]
async fn base64_payload_with_media_type() {
let resp = get("data:text/javascript;base64,RElSRUNU").await.unwrap();
assert_eq!(content_type(&resp), crate::mime::TEXT_JAVASCRIPT);
assert_eq!(resp.try_into_string().await.unwrap(), "DIRECT");
}
#[tokio::test]
async fn media_type_is_preserved() {
let resp = get("data:application/x-ns-proxy-autoconfig,DIRECT")
.await
.unwrap();
assert_eq!(
content_type(&resp).as_ref(),
"application/x-ns-proxy-autoconfig"
);
}
#[tokio::test]
async fn payload_keeps_query_and_drops_fragment() {
let resp = get("data:application/x-ns-proxy-autoconfig,return h==host?DIRECT:PROXY")
.await
.unwrap();
assert_eq!(
resp.try_into_string().await.unwrap(),
"return h==host?DIRECT:PROXY"
);
let resp = get("data:,a?b#c").await.unwrap();
assert_eq!(resp.try_into_string().await.unwrap(), "a?b");
}
#[tokio::test]
async fn head_has_no_body() {
let resp = service()
.serve(
Request::builder()
.method(Method::HEAD)
.uri("data:,hello")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.try_into_string().await.unwrap(), "");
}
#[tokio::test]
async fn missing_comma_errors() {
let _err = get("data:text/plain").await.unwrap_err();
}
#[tokio::test]
async fn invalid_media_type_errors() {
let _err = get("data:not a mime,payload").await.unwrap_err();
}
#[tokio::test]
async fn invalid_base64_errors() {
let _err = get("data:text/plain;base64,!!!not-base64!!!")
.await
.unwrap_err();
}
#[tokio::test]
async fn rejects_non_get_methods() {
let err = service()
.serve(
Request::builder()
.method(Method::POST)
.uri("data:,x")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap_err();
assert!(err.to_string().contains("GET and HEAD"), "{err}");
}
}