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