Skip to content

Commit 03024e2

Browse files
committed
refactor(http): extract function to get client IP on reverse proxy
1 parent 20f5751 commit 03024e2

File tree

10 files changed

+255
-49
lines changed

10 files changed

+255
-49
lines changed

src/http/axum_implementation/handlers.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use std::sync::Arc;
22

33
use axum::extract::State;
44
use axum::response::Json;
5+
use log::debug;
56

6-
use super::extractors::ExtractAnnounceParams;
7+
use super::requests::announce::ExtractAnnounceRequest;
78
use super::resources::ok::Ok;
89
use super::responses::ok_response;
910
use crate::tracker::Tracker;
@@ -13,13 +14,29 @@ pub async fn get_status_handler() -> Json<Ok> {
1314
ok_response()
1415
}
1516

16-
/// # Panics
17-
///
18-
/// todo
17+
/// WIP
1918
#[allow(clippy::unused_async)]
2019
pub async fn announce_handler(
2120
State(_tracker): State<Arc<Tracker>>,
22-
ExtractAnnounceParams(_announce_params): ExtractAnnounceParams,
21+
ExtractAnnounceRequest(announce_request): ExtractAnnounceRequest,
2322
) -> Json<Ok> {
24-
todo!()
23+
/* todo:
24+
- Extract remote client ip from request
25+
- Build the `Peer`
26+
- Call the `tracker.announce` method
27+
- Send event for stats
28+
- Move response from Warp to shared mod
29+
- Send response
30+
*/
31+
32+
// Sample announce URL used for debugging:
33+
// http://0.0.0.0:7070/announce?info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_id=-qB00000000000000001&port=17548
34+
35+
debug!("http announce request: {:#?}", announce_request);
36+
37+
let info_hash = announce_request.info_hash;
38+
39+
debug!("info_hash: {:#?}", &info_hash);
40+
41+
ok_response()
2542
}

src/http/axum_implementation/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
pub mod extractors;
21
pub mod handlers;
32
pub mod query;
3+
pub mod requests;
44
pub mod resources;
55
pub mod responses;
66
pub mod routes;

src/http/axum_implementation/extractors.rs renamed to src/http/axum_implementation/requests/announce.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ use axum::http::request::Parts;
77
use axum::http::StatusCode;
88
use thiserror::Error;
99

10-
use super::query::Query;
10+
use crate::http::axum_implementation::query::Query;
1111
use crate::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
1212
use crate::protocol::info_hash::{ConversionError, InfoHash};
1313
use crate::tracker::peer::{self, IdConversionError};
1414

15-
pub struct ExtractAnnounceParams(pub AnnounceParams);
15+
pub struct ExtractAnnounceRequest(pub Announce);
1616

1717
#[derive(Debug, PartialEq)]
18-
pub struct AnnounceParams {
18+
pub struct Announce {
1919
pub info_hash: InfoHash,
2020
pub peer_id: peer::Id,
2121
pub port: u16,
@@ -55,7 +55,7 @@ impl From<ConversionError> for ParseAnnounceQueryError {
5555
}
5656
}
5757

58-
impl TryFrom<Query> for AnnounceParams {
58+
impl TryFrom<Query> for Announce {
5959
type Error = ParseAnnounceQueryError;
6060

6161
fn try_from(query: Query) -> Result<Self, Self::Error> {
@@ -103,13 +103,15 @@ fn extract_port(query: &Query) -> Result<u16, ParseAnnounceQueryError> {
103103
}
104104

105105
#[async_trait]
106-
impl<S> FromRequestParts<S> for ExtractAnnounceParams
106+
impl<S> FromRequestParts<S> for ExtractAnnounceRequest
107107
where
108108
S: Send + Sync,
109109
{
110110
type Rejection = (StatusCode, &'static str);
111111

112112
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
113+
// todo: error responses body should be bencoded
114+
113115
let raw_query = parts.uri.query();
114116

115117
if raw_query.is_none() {
@@ -122,34 +124,34 @@ where
122124
return Err((StatusCode::BAD_REQUEST, "can't parse query params"));
123125
}
124126

125-
let announce_params = AnnounceParams::try_from(query.unwrap());
127+
let announce_request = Announce::try_from(query.unwrap());
126128

127-
if announce_params.is_err() {
129+
if announce_request.is_err() {
128130
return Err((StatusCode::BAD_REQUEST, "can't parse query params for announce request"));
129131
}
130132

131-
Ok(ExtractAnnounceParams(announce_params.unwrap()))
133+
Ok(ExtractAnnounceRequest(announce_request.unwrap()))
132134
}
133135
}
134136

135137
#[cfg(test)]
136138
mod tests {
137-
use super::AnnounceParams;
139+
use super::Announce;
138140
use crate::http::axum_implementation::query::Query;
139141
use crate::protocol::info_hash::InfoHash;
140142
use crate::tracker::peer;
141143

142144
#[test]
143-
fn announce_request_params_should_be_extracted_from_url_query_params() {
145+
fn announce_request_should_be_extracted_from_url_query_params() {
144146
let raw_query = "info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_id=-qB00000000000000001&port=17548";
145147

146148
let query = raw_query.parse::<Query>().unwrap();
147149

148-
let announce_params = AnnounceParams::try_from(query).unwrap();
150+
let announce_request = Announce::try_from(query).unwrap();
149151

150152
assert_eq!(
151-
announce_params,
152-
AnnounceParams {
153+
announce_request,
154+
Announce {
153155
info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(),
154156
peer_id: "-qB00000000000000001".parse::<peer::Id>().unwrap(),
155157
port: 17548,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod announce;

src/http/handlers/announce.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub fn handler() {}

src/http/handlers/mod.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use std::net::{AddrParseError, IpAddr};
2+
use std::panic::Location;
3+
use std::str::FromStr;
4+
5+
use thiserror::Error;
6+
7+
use crate::located_error::{Located, LocatedError};
8+
9+
pub mod announce;
10+
11+
#[derive(Error, Debug)]
12+
pub enum XForwardedForParseError {
13+
#[error("Empty X-Forwarded-For header value, {location}")]
14+
EmptyValue { location: &'static Location<'static> },
15+
16+
#[error("Invalid IP in X-Forwarded-For header: {source}")]
17+
InvalidIp { source: LocatedError<'static, AddrParseError> },
18+
}
19+
20+
impl From<AddrParseError> for XForwardedForParseError {
21+
#[track_caller]
22+
fn from(err: AddrParseError) -> Self {
23+
Self::InvalidIp {
24+
source: Located(err).into(),
25+
}
26+
}
27+
}
28+
29+
/// It extracts the last IP address from the `X-Forwarded-For` http header value.
30+
///
31+
/// # Errors
32+
///
33+
/// Will return and error if the last IP in the `X-Forwarded-For` header is not a valid IP
34+
pub fn maybe_rightmost_forwarded_ip(x_forwarded_for_value: &str) -> Result<IpAddr, XForwardedForParseError> {
35+
let mut x_forwarded_for_raw = x_forwarded_for_value.to_string();
36+
37+
// Remove whitespace chars
38+
x_forwarded_for_raw.retain(|c| !c.is_whitespace());
39+
40+
// Get all forwarded IP's in a vec
41+
let x_forwarded_ips: Vec<&str> = x_forwarded_for_raw.split(',').collect();
42+
43+
match x_forwarded_ips.last() {
44+
Some(last_ip) => match IpAddr::from_str(last_ip) {
45+
Ok(ip) => Ok(ip),
46+
Err(err) => Err(err.into()),
47+
},
48+
None => Err(XForwardedForParseError::EmptyValue {
49+
location: Location::caller(),
50+
}),
51+
}
52+
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
57+
use std::net::IpAddr;
58+
use std::str::FromStr;
59+
60+
use crate::http::handlers::maybe_rightmost_forwarded_ip;
61+
62+
#[test]
63+
fn the_last_forwarded_ip_can_be_parsed_from_the_the_corresponding_http_header() {
64+
assert!(maybe_rightmost_forwarded_ip("").is_err());
65+
66+
assert!(maybe_rightmost_forwarded_ip("INVALID IP").is_err());
67+
68+
assert_eq!(
69+
maybe_rightmost_forwarded_ip("2001:db8:85a3:8d3:1319:8a2e:370:7348").unwrap(),
70+
IpAddr::from_str("2001:db8:85a3:8d3:1319:8a2e:370:7348").unwrap()
71+
);
72+
73+
assert_eq!(
74+
maybe_rightmost_forwarded_ip("203.0.113.195").unwrap(),
75+
IpAddr::from_str("203.0.113.195").unwrap()
76+
);
77+
78+
assert_eq!(
79+
maybe_rightmost_forwarded_ip("203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348").unwrap(),
80+
IpAddr::from_str("2001:db8:85a3:8d3:1319:8a2e:370:7348").unwrap()
81+
);
82+
83+
assert_eq!(
84+
maybe_rightmost_forwarded_ip("203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178").unwrap(),
85+
IpAddr::from_str("150.172.238.178").unwrap()
86+
);
87+
}
88+
}

src/http/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use serde::{Deserialize, Serialize};
1414

1515
pub mod axum_implementation;
16+
pub mod handlers;
1617
pub mod percent_encoding;
1718
pub mod warp_implementation;
1819

src/http/warp_implementation/filters.rs

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use std::convert::Infallible;
22
use std::net::{IpAddr, SocketAddr};
33
use std::panic::Location;
4-
use std::str::FromStr;
54
use std::sync::Arc;
65

76
use warp::{reject, Filter, Rejection};
87

98
use super::error::Error;
109
use super::{request, WebResult};
10+
use crate::http::handlers::maybe_rightmost_forwarded_ip;
1111
use crate::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
1212
use crate::protocol::common::MAX_SCRAPE_TORRENTS;
1313
use crate::protocol::info_hash::InfoHash;
@@ -138,41 +138,33 @@ fn peer_id(raw_query: &String) -> WebResult<peer::Id> {
138138
}
139139
}
140140

141-
/// Get `PeerAddress` from `RemoteAddress` or Forwarded
142-
fn peer_addr((on_reverse_proxy, remote_addr, x_forwarded_for): (bool, Option<SocketAddr>, Option<String>)) -> WebResult<IpAddr> {
143-
if !on_reverse_proxy && remote_addr.is_none() {
144-
return Err(reject::custom(Error::AddressNotFound {
145-
location: Location::caller(),
146-
message: "neither on have remote address or on a reverse proxy".to_string(),
147-
}));
148-
}
141+
/// Get peer IP from HTTP client IP or X-Forwarded-For HTTP header
142+
fn peer_addr(
143+
(on_reverse_proxy, remote_client_ip, maybe_x_forwarded_for): (bool, Option<SocketAddr>, Option<String>),
144+
) -> WebResult<IpAddr> {
145+
if on_reverse_proxy {
146+
if maybe_x_forwarded_for.is_none() {
147+
return Err(reject::custom(Error::AddressNotFound {
148+
location: Location::caller(),
149+
message: "must have a x-forwarded-for when using a reverse proxy".to_string(),
150+
}));
151+
}
149152

150-
if on_reverse_proxy && x_forwarded_for.is_none() {
151-
return Err(reject::custom(Error::AddressNotFound {
152-
location: Location::caller(),
153-
message: "must have a x-forwarded-for when using a reverse proxy".to_string(),
154-
}));
155-
}
153+
let x_forwarded_for = maybe_x_forwarded_for.unwrap();
156154

157-
if on_reverse_proxy {
158-
let mut x_forwarded_for_raw = x_forwarded_for.unwrap();
159-
// remove whitespace chars
160-
x_forwarded_for_raw.retain(|c| !c.is_whitespace());
161-
// get all forwarded ip's in a vec
162-
let x_forwarded_ips: Vec<&str> = x_forwarded_for_raw.split(',').collect();
163-
// set client ip to last forwarded ip
164-
let x_forwarded_ip = *x_forwarded_ips.last().unwrap();
165-
166-
IpAddr::from_str(x_forwarded_ip).map_err(|e| {
155+
maybe_rightmost_forwarded_ip(&x_forwarded_for).map_err(|e| {
167156
reject::custom(Error::AddressNotFound {
168157
location: Location::caller(),
169-
message: format!(
170-
"on remote proxy and unable to parse the last x-forwarded-ip: `{e}`, from `{x_forwarded_for_raw}`"
171-
),
158+
message: format!("on remote proxy and unable to parse the last x-forwarded-ip: `{e}`, from `{x_forwarded_for}`"),
172159
})
173160
})
161+
} else if remote_client_ip.is_none() {
162+
return Err(reject::custom(Error::AddressNotFound {
163+
location: Location::caller(),
164+
message: "neither on have remote address or on a reverse proxy".to_string(),
165+
}));
174166
} else {
175-
Ok(remote_addr.unwrap().ip())
167+
return Ok(remote_client_ip.unwrap().ip());
176168
}
177169
}
178170

tests/http/asserts.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,23 @@ pub async fn assert_invalid_authentication_key_error_response(response: Response
127127

128128
assert_error_bencoded(&response.text().await.unwrap(), "is not valid", Location::caller());
129129
}
130+
131+
pub async fn assert_could_not_find_remote_address_on_xff_header_error_response(response: Response) {
132+
assert_eq!(response.status(), 200);
133+
134+
assert_error_bencoded(
135+
&response.text().await.unwrap(),
136+
"could not find remote address: must have a x-forwarded-for when using a reverse proxy",
137+
Location::caller(),
138+
);
139+
}
140+
141+
pub async fn assert_invalid_remote_address_on_xff_header_error_response(response: Response) {
142+
assert_eq!(response.status(), 200);
143+
144+
assert_error_bencoded(
145+
&response.text().await.unwrap(),
146+
"could not find remote address: on remote proxy and unable to parse the last x-forwarded-ip",
147+
Location::caller(),
148+
);
149+
}

0 commit comments

Comments
 (0)