@@ -27,22 +27,49 @@ pub fn extract_service_method(uri: &Uri) -> (&str, &str) {
2727 ( service, method)
2828}
2929
30- fn parse_x_forwarded_for ( headers : & HeaderMap ) -> Option < & str > {
30+ #[ must_use]
31+ // From [X-Forwarded-For - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
32+ // > If a request goes through multiple proxies, the IP addresses of each successive proxy is listed.
33+ // > This means that, given well-behaved client and proxies,
34+ // > the rightmost IP address is the IP address of the most recent proxy and
35+ // > the leftmost IP address is the IP address of the originating client.
36+ pub fn extract_client_ip_from_headers ( headers : & HeaderMap ) -> Option < & str > {
37+ extract_client_ip_from_forwarded ( headers)
38+ . or_else ( || extract_client_ip_from_x_forwarded_for ( headers) )
39+ }
40+
41+ #[ must_use]
42+ // From [X-Forwarded-For - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
43+ // > If a request goes through multiple proxies, the IP addresses of each successive proxy is listed.
44+ // > This means that, given well-behaved client and proxies,
45+ // > the rightmost IP address is the IP address of the most recent proxy and
46+ // > the leftmost IP address is the IP address of the originating client.
47+ fn extract_client_ip_from_x_forwarded_for ( headers : & HeaderMap ) -> Option < & str > {
3148 let value = headers. get ( "x-forwarded-for" ) ?;
3249 let value = value. to_str ( ) . ok ( ) ?;
3350 let mut ips = value. split ( ',' ) ;
3451 Some ( ips. next ( ) ?. trim ( ) )
3552}
3653
37- #[ inline]
38- pub fn client_ip < B > ( req : & http:: Request < B > ) -> & str {
39- parse_x_forwarded_for ( req. headers ( ) )
40- // .or_else(|| {
41- // req.extensions()
42- // .get::<ConnectInfo<SocketAddr>>()
43- // .map(|ConnectInfo(client_ip)| Cow::from(client_ip.to_string()))
44- // })
45- . unwrap_or_default ( )
54+ #[ must_use]
55+ // see [Forwarded header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Forwarded)
56+ fn extract_client_ip_from_forwarded ( headers : & HeaderMap ) -> Option < & str > {
57+ let value = headers. get ( "forwarded" ) ?;
58+ let value = value. to_str ( ) . ok ( ) ?;
59+ value
60+ . split ( ';' )
61+ . flat_map ( |directive| directive. split ( ',' ) )
62+ // select the left/first "for" key
63+ . find_map ( |directive| directive. trim ( ) . strip_prefix ( "for=" ) )
64+ // ipv6 are enclosed into `["..."]`
65+ // string are enclosed into `"..."`
66+ . map ( |directive| {
67+ directive
68+ . trim_start_matches ( '[' )
69+ . trim_end_matches ( ']' )
70+ . trim_matches ( '"' )
71+ . trim ( )
72+ } )
4673}
4774
4875#[ inline]
@@ -128,4 +155,57 @@ mod tests {
128155 let uri: Uri = input. parse ( ) . unwrap ( ) ;
129156 assert ! ( url_scheme( & uri) == expected) ;
130157 }
158+
159+ #[ rstest]
160+ #[ case( "" , "" ) ]
161+ #[ case(
162+ "2001:db8:85a3:8d3:1319:8a2e:370:7348" ,
163+ "2001:db8:85a3:8d3:1319:8a2e:370:7348"
164+ ) ]
165+ #[ case( "203.0.113.195" , "203.0.113.195" ) ]
166+ #[ case( "203.0.113.195,10.10.10.10" , "203.0.113.195" ) ]
167+ #[ case( "203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348" , "203.0.113.195" ) ]
168+ fn test_extract_client_ip_from_x_forwarded_for ( #[ case] input : & str , #[ case] expected : & str ) {
169+ let mut headers = HeaderMap :: new ( ) ;
170+ if !input. is_empty ( ) {
171+ headers. insert ( "X-Forwarded-For" , input. parse ( ) . unwrap ( ) ) ;
172+ }
173+
174+ let expected = if expected. is_empty ( ) {
175+ None
176+ } else {
177+ Some ( expected)
178+ } ;
179+ assert ! ( extract_client_ip_from_x_forwarded_for( & headers) == expected) ;
180+ }
181+
182+ #[ rstest]
183+ #[ case( "" , "" ) ]
184+ #[ case(
185+ "for=[\" 2001:db8:85a3:8d3:1319:8a2e:370:7348\" ]" ,
186+ "2001:db8:85a3:8d3:1319:8a2e:370:7348"
187+ ) ]
188+ #[ case( "for=203.0.113.195" , "203.0.113.195" ) ]
189+ #[ case( "for=203.0.113.195, for=10.10.10.10" , "203.0.113.195" ) ]
190+ #[ case(
191+ "for=203.0.113.195, for=[\" 2001:db8:85a3:8d3:1319:8a2e:370:7348\" ]" ,
192+ "203.0.113.195"
193+ ) ]
194+ #[ case( "for=\" _mdn\" " , "_mdn" ) ]
195+ #[ case( "for=\" secret\" " , "secret" ) ]
196+ #[ case( "for=203.0.113.195;proto=http;by=203.0.113.43" , "203.0.113.195" ) ]
197+ #[ case( "proto=http;by=203.0.113.43" , "" ) ]
198+ fn test_extract_client_ip_from_forwarded ( #[ case] input : & str , #[ case] expected : & str ) {
199+ let mut headers = HeaderMap :: new ( ) ;
200+ if !input. is_empty ( ) {
201+ headers. insert ( "Forwarded" , input. parse ( ) . unwrap ( ) ) ;
202+ }
203+
204+ let expected = if expected. is_empty ( ) {
205+ None
206+ } else {
207+ Some ( expected)
208+ } ;
209+ assert ! ( extract_client_ip_from_forwarded( & headers) == expected) ;
210+ }
131211}
0 commit comments