actix_web_lab/
x_forwarded_prefix.rs1use 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#[allow(clippy::declare_interior_mutable_const)]
20pub const X_FORWARDED_PREFIX: HeaderName = HeaderName::from_static("x-forwarded-prefix");
21
22#[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#[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}