actix_web_lab/
x_forwarded_prefix.rs

1//! X-Forwarded-Prefix header.
2//!
3//! See [`XForwardedPrefix`] docs.
4
5use std::future::{Ready, ready};
6
7use actix_http::{
8    HttpMessage,
9    error::ParseError,
10    header::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue},
11};
12use actix_web::FromRequest;
13use derive_more::Display;
14use http::uri::PathAndQuery;
15
16/// Conventional `X-Forwarded-Prefix` header.
17///
18/// See <https://github.com/dotnet/aspnetcore/issues/23263#issuecomment-776192575>.
19#[allow(clippy::declare_interior_mutable_const)]
20pub const X_FORWARDED_PREFIX: HeaderName = HeaderName::from_static("x-forwarded-prefix");
21
22/// The conventional `X-Forwarded-Prefix` header.
23///
24/// The `X-Forwarded-Prefix` header field is used to signal that a prefix was stripped from the path
25/// while being proxied.
26///
27/// # Example Values
28///
29/// - `/`
30/// - `/foo`
31///
32/// # Examples
33///
34/// ```
35/// use actix_web::HttpResponse;
36/// use actix_web_lab::header::XForwardedPrefix;
37///
38/// let mut builder = HttpResponse::Ok();
39/// builder.insert_header(XForwardedPrefix("/bar".parse().unwrap()));
40/// ```
41#[derive(Debug, Clone, PartialEq, Eq, Display)]
42pub struct XForwardedPrefix(pub PathAndQuery);
43
44impl_more::impl_deref_and_mut!(XForwardedPrefix => PathAndQuery);
45
46impl TryIntoHeaderValue for XForwardedPrefix {
47    type Error = InvalidHeaderValue;
48
49    fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
50        HeaderValue::try_from(self.to_string())
51    }
52}
53
54impl Header for XForwardedPrefix {
55    fn name() -> HeaderName {
56        X_FORWARDED_PREFIX
57    }
58
59    fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError> {
60        let header = msg.headers().get(Self::name());
61
62        header
63            .and_then(|hdr| hdr.to_str().ok())
64            .map(|hdr| hdr.trim())
65            .filter(|hdr| !hdr.is_empty())
66            .and_then(|hdr| hdr.parse::<actix_web::http::uri::PathAndQuery>().ok())
67            .filter(|path| path.query().is_none())
68            .map(XForwardedPrefix)
69            .ok_or(ParseError::Header)
70    }
71}
72
73#[cfg(test)]
74mod header_tests {
75    use actix_web::test::{self};
76
77    use super::*;
78
79    #[test]
80    fn deref() {
81        let mut fwd_prefix = XForwardedPrefix(PathAndQuery::from_static("/"));
82        let _: &PathAndQuery = &fwd_prefix;
83        let _: &mut PathAndQuery = &mut fwd_prefix;
84    }
85
86    #[test]
87    fn no_headers() {
88        let req = test::TestRequest::default().to_http_request();
89        assert_eq!(XForwardedPrefix::parse(&req).ok(), None);
90    }
91
92    #[test]
93    fn empty_header() {
94        let req = test::TestRequest::default()
95            .insert_header((X_FORWARDED_PREFIX, ""))
96            .to_http_request();
97
98        assert_eq!(XForwardedPrefix::parse(&req).ok(), None);
99    }
100
101    #[test]
102    fn single_header() {
103        let req = test::TestRequest::default()
104            .insert_header((X_FORWARDED_PREFIX, "/foo"))
105            .to_http_request();
106
107        assert_eq!(
108            XForwardedPrefix::parse(&req).ok().unwrap(),
109            XForwardedPrefix(PathAndQuery::from_static("/foo")),
110        );
111    }
112
113    #[test]
114    fn multiple_headers() {
115        let req = test::TestRequest::default()
116            .append_header((X_FORWARDED_PREFIX, "/foo"))
117            .append_header((X_FORWARDED_PREFIX, "/bar"))
118            .to_http_request();
119
120        assert_eq!(
121            XForwardedPrefix::parse(&req).ok().unwrap(),
122            XForwardedPrefix(PathAndQuery::from_static("/foo")),
123        );
124    }
125}
126
127/// Reconstructed path using X-Forwarded-Prefix header.
128///
129/// ```
130/// # use actix_web::{FromRequest as _, test::TestRequest};
131/// # actix_web::rt::System::new().block_on(async {
132/// use actix_web_lab::extract::ReconstructedPath;
133///
134/// let req = TestRequest::with_uri("/bar")
135///     .insert_header(("x-forwarded-prefix", "/foo"))
136///     .to_http_request();
137///
138/// let path = ReconstructedPath::extract(&req).await.unwrap();
139/// assert_eq!(path.to_string(), "/foo/bar");
140/// # })
141/// ```
142#[derive(Debug, Clone, PartialEq, Eq, Display)]
143pub struct ReconstructedPath(pub PathAndQuery);
144
145impl FromRequest for ReconstructedPath {
146    type Error = actix_web::Error;
147    type Future = Ready<Result<Self, Self::Error>>;
148
149    fn from_request(
150        req: &actix_web::HttpRequest,
151        _payload: &mut actix_http::Payload,
152    ) -> Self::Future {
153        let parts = req.head().uri.clone().into_parts();
154        let path_and_query = parts
155            .path_and_query
156            .unwrap_or(PathAndQuery::from_static("/"));
157
158        let prefix = XForwardedPrefix::parse(req).unwrap();
159
160        let reconstructed = [prefix.as_str(), path_and_query.as_str()].concat();
161
162        ready(Ok(ReconstructedPath(
163            PathAndQuery::from_maybe_shared(reconstructed).unwrap(),
164        )))
165    }
166}
167
168#[cfg(test)]
169mod extractor_tests {
170    use actix_web::test::{self};
171
172    use super::*;
173
174    #[actix_web::test]
175    async fn basic() {
176        let req = test::TestRequest::with_uri("/bar")
177            .insert_header((X_FORWARDED_PREFIX, "/foo"))
178            .to_http_request();
179
180        assert_eq!(
181            ReconstructedPath::extract(&req).await.unwrap(),
182            ReconstructedPath(PathAndQuery::from_static("/foo/bar")),
183        );
184    }
185}