Skip to content

Commit

Permalink
feat(http): [#184] Axum extractor for peer IP
Browse files Browse the repository at this point in the history
It uses a wrapper for another extractor becuase that extractor cannot be
optional. We need to get the rigth most IP in the X-Forwarded-For header
only when the tracker is runnin gon reverse proxy.

More info:

imbolc/axum-client-ip#9 (comment)
  • Loading branch information
josecelano committed Feb 16, 2023
1 parent 74ed592 commit 02e2516
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 38 deletions.
2 changes: 2 additions & 0 deletions src/http/axum_implementation/extractors/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod peer_ip;
pub mod remote_client_ip;
52 changes: 52 additions & 0 deletions src/http/axum_implementation/extractors/peer_ip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use std::net::IpAddr;
use std::panic::Location;

use axum::response::{IntoResponse, Response};
use thiserror::Error;

use super::remote_client_ip::RemoteClientIp;
use crate::http::axum_implementation::responses;

#[derive(Error, Debug)]
pub enum ResolutionError {
#[error("missing the right most X-Forwarded-For IP (mandatory on reverse proxy tracker configuration) in {location}")]
MissingRightMostXForwardedForIp { location: &'static Location<'static> },
#[error("cannot get the client IP from the connection info in {location}")]
MissingClientIp { location: &'static Location<'static> },
}

impl From<ResolutionError> for responses::error::Error {
fn from(err: ResolutionError) -> Self {
responses::error::Error {
failure_reason: format!("{err}"),
}
}
}

/// It resolves the peer IP.
///
/// # Errors
///
/// Will return an error if the peer IP cannot be obtained according to the configuration.
/// For example, if the IP is extracted from an HTTP header which is missing in the request.
pub fn peer_ip(on_reverse_proxy: bool, remote_client_ip: &RemoteClientIp) -> Result<IpAddr, Response> {
if on_reverse_proxy {
if let Some(ip) = remote_client_ip.right_most_x_forwarded_for {
Ok(ip)
} else {
Err(
responses::error::Error::from(ResolutionError::MissingRightMostXForwardedForIp {
location: Location::caller(),
})
.into_response(),
)
}
} else if let Some(ip) = remote_client_ip.connection_info_ip {
Ok(ip)
} else {
Err(responses::error::Error::from(ResolutionError::MissingClientIp {
location: Location::caller(),
})
.into_response())
}
}
51 changes: 51 additions & 0 deletions src/http/axum_implementation/extractors/remote_client_ip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use std::net::{IpAddr, SocketAddr};

use axum::async_trait;
use axum::extract::{ConnectInfo, FromRequestParts};
use axum::http::request::Parts;
use axum::response::Response;
use axum_client_ip::RightmostXForwardedFor;
use serde::{Deserialize, Serialize};

/// Given this request chain:
///
/// client <-> http proxy 1 <-> http proxy 2 <-> server
/// ip: 126.0.0.1 ip: 126.0.0.2 ip: 126.0.0.3 ip: 126.0.0.4
/// X-Forwarded-For: 126.0.0.1 X-Forwarded-For: 126.0.0.1,126.0.0.2
///
/// This extractor extracts these values from the HTTP headers and connection info.
///
/// `right_most_x_forwarded_for` = 126.0.0.2
/// `connection_info_ip` = 126.0.0.1
///
/// More info about inner extractors :<https://github.com/imbolc/axum-client-ip>
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct RemoteClientIp {
pub right_most_x_forwarded_for: Option<IpAddr>,
pub connection_info_ip: Option<IpAddr>,
}

#[async_trait]
impl<S> FromRequestParts<S> for RemoteClientIp
where
S: Send + Sync,
{
type Rejection = Response;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let right_most_x_forwarded_for = match RightmostXForwardedFor::from_request_parts(parts, state).await {
Ok(right_most_x_forwarded_for) => Some(right_most_x_forwarded_for.0),
Err(_) => None,
};

let connection_info_ip = match ConnectInfo::<SocketAddr>::from_request_parts(parts, state).await {
Ok(connection_info_socket_addr) => Some(connection_info_socket_addr.0.ip()),
Err(_) => None,
};

Ok(RemoteClientIp {
right_most_x_forwarded_for,
connection_info_ip,
})
}
}
20 changes: 13 additions & 7 deletions src/http/axum_implementation/handlers/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,38 @@ use std::sync::Arc;
use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes};
use axum::extract::State;
use axum::response::{IntoResponse, Response};
use axum_client_ip::SecureClientIp;
use log::debug;

use crate::http::axum_implementation::extractors::peer_ip::peer_ip;
use crate::http::axum_implementation::extractors::remote_client_ip::RemoteClientIp;
use crate::http::axum_implementation::requests::announce::{Announce, Event, ExtractAnnounceRequest};
use crate::http::axum_implementation::responses;
use crate::protocol::clock::{Current, Time};
use crate::tracker::peer::Peer;
use crate::tracker::{statistics, Tracker};

/// WIP
#[allow(clippy::unused_async)]
pub async fn handle(
State(tracker): State<Arc<Tracker>>,
ExtractAnnounceRequest(announce_request): ExtractAnnounceRequest,
secure_ip: SecureClientIp,
remote_client_ip: RemoteClientIp,
) -> Response {
debug!("http announce request: {:#?}", announce_request);

let info_hash = announce_request.info_hash;
let remote_client_ip = secure_ip.0;

let mut peer = peer_from_request(&announce_request, &remote_client_ip);
let peer_ip = peer_ip(tracker.config.on_reverse_proxy, &remote_client_ip);

let response = tracker.announce(&info_hash, &mut peer, &remote_client_ip).await;
let peer_ip = match peer_ip {
Ok(peer_ip) => peer_ip,
Err(err) => return err,
};

match remote_client_ip {
let mut peer = peer_from_request(&announce_request, &peer_ip);

let response = tracker.announce(&info_hash, &mut peer, &peer_ip).await;

match peer_ip {
IpAddr::V4(_) => {
tracker.send_stats_event(statistics::Event::Tcp4Announce).await;
}
Expand Down
6 changes: 3 additions & 3 deletions src/http/axum_implementation/handlers/status.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/// Temporary handler for testing and debugging the new Axum implementation
/// It should be removed once the migration to Axum is finished.
use axum::response::Json;
use axum_client_ip::{InsecureClientIp, SecureClientIp};

use crate::http::axum_implementation::extractors::remote_client_ip::RemoteClientIp;
use crate::http::axum_implementation::resources::ok::Ok;
use crate::http::axum_implementation::responses::ok;

#[allow(clippy::unused_async)]
pub async fn get_status_handler(insecure_ip: InsecureClientIp, secure_ip: SecureClientIp) -> Json<Ok> {
ok::response(&insecure_ip.0, &secure_ip.0)
pub async fn get_status_handler(remote_client_ip: RemoteClientIp) -> Json<Ok> {
ok::response(&remote_client_ip)
}
1 change: 1 addition & 0 deletions src/http/axum_implementation/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod extractors;
pub mod handlers;
pub mod query;
pub mod requests;
Expand Down
8 changes: 5 additions & 3 deletions src/http/axum_implementation/requests/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ impl FromStr for Compact {

#[derive(Error, Debug)]
pub enum ParseAnnounceQueryError {
#[error("missing query params for announce request in {location}")]
MissingParams { location: &'static Location<'static> },
#[error("missing param {param_name} in {location}")]
MissingParam {
location: &'static Location<'static>,
Expand Down Expand Up @@ -290,9 +292,9 @@ where
let raw_query = parts.uri.query();

if raw_query.is_none() {
return Err(responses::error::Error {
failure_reason: "missing query params for announce request".to_string(),
}
return Err(responses::error::Error::from(ParseAnnounceQueryError::MissingParams {
location: Location::caller(),
})
.into_response());
}

Expand Down
7 changes: 3 additions & 4 deletions src/http/axum_implementation/resources/ok.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use std::net::IpAddr;

use serde::{Deserialize, Serialize};

use crate::http::axum_implementation::extractors::remote_client_ip::RemoteClientIp;

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Ok {
pub remote_client_insecure_ip: IpAddr,
pub remote_client_secure_ip: IpAddr,
pub remote_client_ip: RemoteClientIp,
}
8 changes: 3 additions & 5 deletions src/http/axum_implementation/responses/ok.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
use std::net::IpAddr;

use axum::Json;

use crate::http::axum_implementation::extractors::remote_client_ip::RemoteClientIp;
use crate::http::axum_implementation::resources::ok::Ok;

#[must_use]
pub fn response(remote_client_insecure_ip: &IpAddr, remote_client_secure_ip: &IpAddr) -> Json<Ok> {
pub fn response(remote_client_ip: &RemoteClientIp) -> Json<Ok> {
Json(Ok {
remote_client_insecure_ip: *remote_client_insecure_ip,
remote_client_secure_ip: *remote_client_secure_ip,
remote_client_ip: remote_client_ip.clone(),
})
}
8 changes: 1 addition & 7 deletions src/http/axum_implementation/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,10 @@ use super::handlers::status::get_status_handler;
use crate::tracker::Tracker;

pub fn router(tracker: &Arc<Tracker>) -> Router {
let secure_client_ip_source = if tracker.config.on_reverse_proxy {
SecureClientIpSource::RightmostXForwardedFor
} else {
SecureClientIpSource::ConnectInfo
};

Router::new()
// Status
.route("/status", get(get_status_handler))
// Announce request
.route("/announce", get(handle).with_state(tracker.clone()))
.layer(secure_client_ip_source.into_extension())
.layer(SecureClientIpSource::ConnectInfo.into_extension())
}
31 changes: 22 additions & 9 deletions tests/http_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,7 @@ mod axum_http_tracker_server {
// WIP: migration HTTP from Warp to Axum

use local_ip_address::local_ip;
use torrust_tracker::http::axum_implementation::extractors::remote_client_ip::RemoteClientIp;
use torrust_tracker::http::axum_implementation::resources::ok::Ok;
use torrust_tracker::http::Version;

Expand All @@ -1287,8 +1288,10 @@ mod axum_http_tracker_server {
assert_eq!(
ok,
Ok {
remote_client_insecure_ip: client_ip,
remote_client_secure_ip: client_ip
remote_client_ip: RemoteClientIp {
right_most_x_forwarded_for: None,
connection_info_ip: Some(client_ip)
}
}
);
}
Expand All @@ -1302,6 +1305,7 @@ mod axum_http_tracker_server {
use std::str::FromStr;

use local_ip_address::local_ip;
use torrust_tracker::http::axum_implementation::extractors::remote_client_ip::RemoteClientIp;
use torrust_tracker::http::axum_implementation::resources::ok::Ok;
use torrust_tracker::http::Version;

Expand All @@ -1323,8 +1327,10 @@ mod axum_http_tracker_server {
assert_eq!(
ok,
Ok {
remote_client_insecure_ip: client_ip,
remote_client_secure_ip: client_ip
remote_client_ip: RemoteClientIp {
right_most_x_forwarded_for: None,
connection_info_ip: Some(client_ip)
}
}
);
}
Expand All @@ -1345,8 +1351,10 @@ mod axum_http_tracker_server {
assert_eq!(
ok,
Ok {
remote_client_insecure_ip: client_ip,
remote_client_secure_ip: client_ip
remote_client_ip: RemoteClientIp {
right_most_x_forwarded_for: None,
connection_info_ip: Some(client_ip)
}
}
);
}
Expand All @@ -1362,7 +1370,10 @@ mod axum_http_tracker_server {

let http_tracker_server = start_http_tracker_on_reverse_proxy(Version::Axum).await;

let client = Client::new(http_tracker_server.get_connection_info());
let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap();
let client_ip = loopback_ip;

let client = Client::bind(http_tracker_server.get_connection_info(), client_ip);

let left_most_ip = IpAddr::from_str("203.0.113.195").unwrap();
let right_most_ip = IpAddr::from_str("150.172.238.178").unwrap();
Expand All @@ -1380,8 +1391,10 @@ mod axum_http_tracker_server {
assert_eq!(
ok,
Ok {
remote_client_insecure_ip: left_most_ip,
remote_client_secure_ip: right_most_ip
remote_client_ip: RemoteClientIp {
right_most_x_forwarded_for: Some(right_most_ip),
connection_info_ip: Some(client_ip)
}
}
);
}
Expand Down

0 comments on commit 02e2516

Please sign in to comment.