Skip to main content

rocket_client_addr/
guard.rs

1use std::{fmt, net::IpAddr};
2
3use rocket::{
4    http::{Status, uncased::Uncased},
5    outcome::Outcome,
6    request::{self, FromRequest, Request},
7};
8
9use crate::{
10    ClientIpConfig,
11    ClientIpRejection::{self, MissingConfig, MissingRemoteAddr},
12};
13
14/// The resolved client IP, and where it came from.
15///
16/// This is a Rocket request guard. It reads a [`ClientIpConfig`] out of Rocket's managed state, so the config has to be passed to `rocket::build().manage(config)`, and it falls back to the address of the connection that Rocket records in [`Request::remote`].
17#[derive(Clone, Debug, Eq, Hash, PartialEq)]
18pub struct ClientIp {
19    ip:     IpAddr,
20    source: ClientIpSource,
21}
22
23impl ClientIp {
24    #[inline]
25    pub(crate) const fn new(ip: IpAddr, source: ClientIpSource) -> Self {
26        Self {
27            ip,
28            source,
29        }
30    }
31
32    /// Return the resolved client IP.
33    #[inline]
34    pub const fn ip(&self) -> IpAddr {
35        self.ip
36    }
37
38    /// Return where the client IP came from.
39    ///
40    /// Use this to tell a header value apart from the socket peer IP, for example when logging.
41    #[inline]
42    pub const fn source(&self) -> &ClientIpSource {
43        &self.source
44    }
45
46    /// Consume this value and return the client IP and its source.
47    #[inline]
48    pub fn into_parts(self) -> (IpAddr, ClientIpSource) {
49        (self.ip, self.source)
50    }
51}
52
53impl fmt::Display for ClientIp {
54    #[inline]
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        fmt::Display::fmt(&self.ip, f)
57    }
58}
59
60impl From<ClientIp> for IpAddr {
61    #[inline]
62    fn from(client_ip: ClientIp) -> Self {
63        client_ip.ip
64    }
65}
66
67/// Where a [`ClientIp`] came from.
68#[derive(Clone, Debug, Eq, Hash, PartialEq)]
69pub enum ClientIpSource {
70    /// The client IP header of a trusted proxy, such as `X-Real-IP`.
71    ///
72    /// The name inside is the header that gave the address.
73    ConfiguredHeader(Uncased<'static>),
74
75    /// One hop of a chain header, such as `X-Forwarded-For` or `Forwarded`.
76    ///
77    /// The name inside is the header that gave the address.
78    ChainHeader(Uncased<'static>),
79
80    /// The socket peer IP, which is the address the connection came from.
81    ///
82    /// This is the answer whenever no header is trusted enough to change it.
83    Socket,
84}
85
86impl ClientIpSource {
87    /// Return the header that gave the address, or [`None`] for [`Self::Socket`].
88    #[inline]
89    pub const fn header_name(&self) -> Option<&Uncased<'static>> {
90        match self {
91            Self::ConfiguredHeader(header) | Self::ChainHeader(header) => Some(header),
92            Self::Socket => None,
93        }
94    }
95}
96
97/// Resolve the client IP of a request, or name the setup mistake that stopped it.
98fn from_request(request: &Request<'_>) -> Result<ClientIp, ClientIpRejection> {
99    let config = request.rocket().state::<ClientIpConfig>().ok_or(MissingConfig)?;
100    let remote = request.remote().ok_or(MissingRemoteAddr)?;
101
102    Ok(config.resolve_client_ip(request.headers(), remote.ip()))
103}
104
105#[rocket::async_trait]
106impl<'r> FromRequest<'r> for ClientIp {
107    type Error = ClientIpRejection;
108
109    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
110        match from_request(request) {
111            Ok(client_ip) => Outcome::Success(client_ip),
112            // Neither failure is caused by the request, so this is a server error rather than a bad request.
113            Err(rejection) => Outcome::Error((Status::InternalServerError, rejection)),
114        }
115    }
116}
117
118#[rocket::async_trait]
119impl<'r> FromRequest<'r> for &'r ClientIp {
120    type Error = ClientIpRejection;
121
122    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
123        // A route may take the client IP more than once, directly or through another guard, and the chain headers are only worth walking once per request.
124        let cache: &Result<ClientIp, ClientIpRejection> =
125            request.local_cache(|| from_request(request));
126
127        match cache {
128            Ok(client_ip) => Outcome::Success(client_ip),
129            Err(rejection) => Outcome::Error((Status::InternalServerError, *rejection)),
130        }
131    }
132}