Skip to main content

rama_http/layer/match_redirect/
service.rs

1use crate::service::web::response::{Headers, IntoResponse};
2use crate::{Body, Request, Response, StatusCode, StreamingBody};
3use rama_core::Service;
4use rama_core::bytes::Bytes;
5use rama_core::error::BoxError;
6use rama_core::telemetry::tracing;
7use rama_http_headers::Location;
8use rama_net::http::uri::{UriMatchError, UriMatchReplace};
9use rama_utils::macros::define_inner_service_accessors;
10use std::borrow::Cow;
11
12/// Middleware to redirect a request using dynamic [`Uri`] derived
13/// from the input request or a static one.
14///
15/// If no match is found it is instead the inner service which
16/// instead makes serves the request and produces a response.
17///
18/// [`Uri`]: rama_net::uri::Uri
19#[derive(Debug, Clone)]
20pub struct UriMatchRedirectService<R, S> {
21    status_code: StatusCode,
22    match_replace: R,
23    inner: S,
24}
25
26impl<R, S> UriMatchRedirectService<R, S> {
27    /// Creates a new "see other" (303) [`UriMatchRedirectService`]
28    /// with the given [`UriMatchReplace`] implementation to optionally redirect
29    /// early returning instead of serving using the inner [`Service`].
30    ///
31    /// [`UriMatchReplace`]: rama_net::http::uri::UriMatchReplace
32    #[inline]
33    pub fn to(match_replace: R, service: S) -> Self {
34        Self::new(StatusCode::SEE_OTHER, match_replace, service)
35    }
36
37    /// Creates a new "moved permanently" (301) [`UriMatchRedirectService`]
38    /// with the given [`UriMatchReplace`] implementation to optionally redirect
39    /// early returning instead of serving using the inner [`Service`].
40    ///
41    /// [`UriMatchReplace`]: rama_net::http::uri::UriMatchReplace
42    #[inline]
43    pub fn moved(match_replace: R, service: S) -> Self {
44        Self::new(StatusCode::MOVED_PERMANENTLY, match_replace, service)
45    }
46
47    /// Creates a new "found" (302) [`UriMatchRedirectService`]
48    /// with the given [`UriMatchReplace`] implementation to optionally redirect
49    /// early returning instead of serving using the inner [`Service`].
50    ///
51    /// [`UriMatchReplace`]: rama_net::http::uri::UriMatchReplace
52    #[inline]
53    pub fn found(match_replace: R, service: S) -> Self {
54        Self::new(StatusCode::FOUND, match_replace, service)
55    }
56
57    /// Creates a new "temporary redirect" (307) [`UriMatchRedirectService`]
58    /// with the given [`UriMatchReplace`] implementation to optionally redirect
59    /// early returning instead of serving using the inner [`Service`].
60    ///
61    /// [`UriMatchReplace`]: rama_net::http::uri::UriMatchReplace
62    #[inline]
63    pub fn temporary(match_replace: R, service: S) -> Self {
64        Self::new(StatusCode::TEMPORARY_REDIRECT, match_replace, service)
65    }
66
67    /// Creates a new "temporary redirect" (307) [`UriMatchRedirectService`]
68    /// with the given [`UriMatchReplace`] implementation to optionally redirect
69    /// early returning instead of serving using the inner [`Service`].
70    ///
71    /// [`UriMatchReplace`]: rama_net::http::uri::UriMatchReplace
72    #[inline]
73    pub fn permanent(match_replace: R, service: S) -> Self {
74        Self::new(StatusCode::PERMANENT_REDIRECT, match_replace, service)
75    }
76
77    pub(super) fn new(status_code: StatusCode, match_replace: R, service: S) -> Self {
78        Self {
79            status_code,
80            match_replace,
81            inner: service,
82        }
83    }
84}
85
86impl<R, S> UriMatchRedirectService<R, S> {
87    define_inner_service_accessors!();
88
89    /// Shared reference to the used [`UriMatchReplace`]
90    #[must_use]
91    pub fn match_replace_ref(&self) -> &R {
92        &self.match_replace
93    }
94
95    /// Exclusive reference to the used [`UriMatchReplace`]
96    #[must_use]
97    pub fn match_replace_mut(&mut self) -> &mut R {
98        &mut self.match_replace
99    }
100}
101
102impl<ReqBody, ResBody, R, S> Service<Request<ReqBody>> for UriMatchRedirectService<R, S>
103where
104    S: Service<Request<ReqBody>, Output = Response<ResBody>>,
105    R: UriMatchReplace + Send + Sync + 'static,
106    ReqBody: Send + 'static,
107    ResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
108{
109    type Output = Response;
110    type Error = S::Error;
111
112    async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
113        let full_uri = req.request_uri();
114        if let Ok(uri) = self
115            .match_replace
116            .match_replace_uri(Cow::Owned(full_uri.clone()))
117            .inspect_err(|err| match err {
118                UriMatchError::NoMatch(uri) => {
119                    tracing::trace!("no match found for uri: {uri}; ignore")
120                }
121                UriMatchError::Unexpected(err) => {
122                    tracing::trace!("unexpected error while trying to match uri: {err}; ignore")
123                }
124            })
125            && *uri != full_uri
126        {
127            return match Location::try_from(uri.as_ref()) {
128                Ok(loc) => {
129                    tracing::debug!(
130                        "redirct request '{full_uri}' to '{uri}' w/ status code {}",
131                        self.status_code
132                    );
133                    Ok((Headers::single(loc), self.status_code).into_response())
134                }
135                Err(err) => {
136                    tracing::debug!(
137                        "failed to send response for redirct request '{full_uri}' to '{uri}' w/ status code {}; loc header encoding failed: {err}",
138                        self.status_code
139                    );
140                    Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
141                }
142            };
143        }
144
145        let resp = self.inner.serve(req).await?;
146        Ok(resp.map(Body::new))
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use crate::{layer::into_response::IntoResponseService, service::web::IntoEndpointService};
153    use rama_http_headers::HeaderMapExt;
154    use rama_net::http::uri::UriMatchReplaceRule;
155    use rama_net::uri::Uri;
156
157    use super::*;
158
159    #[tokio::test]
160    async fn test_redirect_svc() {
161        let svc = UriMatchRedirectService::moved(
162            [
163                UriMatchReplaceRule::http_to_https(),
164                UriMatchReplaceRule::try_new("https://www.*", "https://$1").unwrap(),
165                UriMatchReplaceRule::try_new("*", "$1").unwrap(), // always matches, but redirect should ignore same uris
166            ],
167            IntoResponseService::new(StatusCode::OK.into_endpoint_service()),
168        );
169
170        for (input_uri, expected_option) in [
171            ("http://example.com", Some("https://example.com")),
172            ("http://example.com/foo", Some("https://example.com/foo")),
173            ("https://www.example.com", Some("https://example.com")),
174            (
175                "https://www.example.com/foo",
176                Some("https://example.com/foo"),
177            ),
178            ("https://example.com", None),
179            ("https://example.com/foo", None),
180        ] {
181            let req = Request::builder()
182                .uri(input_uri)
183                .body(Body::empty())
184                .unwrap();
185            let resp = svc.serve(req).await.unwrap();
186            match expected_option {
187                Some(loc) => {
188                    assert_eq!(StatusCode::MOVED_PERMANENTLY, resp.status());
189                    assert_eq!(
190                        resp.headers()
191                            .typed_get::<Location>()
192                            .and_then(|loc| loc.to_str().ok().and_then(|s| Uri::try_from(s).ok())),
193                        Some(Uri::from_static(loc)),
194                    );
195                }
196                None => {
197                    assert_eq!(StatusCode::OK, resp.status());
198                }
199            }
200        }
201    }
202}