siderite_core/middleware/
proxy.rs1use crate::extract::Request;
4use std::collections::HashSet;
5use std::net::{IpAddr, SocketAddr};
6use std::sync::Arc;
7
8type ClientIp = Result<Option<IpAddr>, ()>;
9type Header<'a> = Result<Option<&'a str>, ()>;
10
11#[derive(Debug, Clone, Copy)]
13pub struct ProxyContext {
14 pub client_ip: Option<IpAddr>,
16 pub scheme: &'static str,
18}
19
20#[derive(Debug, thiserror::Error)]
22#[error("invalid forwarded request context")]
23pub struct ProxyError;
24
25#[derive(Debug, Clone, Default)]
31pub struct TrustedProxies {
32 peers: Arc<HashSet<IpAddr>>,
33 all: bool,
34}
35
36impl TrustedProxies {
37 pub fn new(peers: impl IntoIterator<Item = IpAddr>) -> Self {
45 let peers = peers.into_iter().map(|ip| ip.to_canonical()).collect();
46 Self {
47 peers: Arc::new(peers),
48 all: false,
49 }
50 }
51
52 pub fn dangerously_trust_all() -> Self {
61 Self {
62 peers: Arc::default(),
63 all: true,
64 }
65 }
66
67 pub fn resolve(&self, req: &Request) -> Result<ProxyContext, ProxyError> {
75 let client_ip = self.client_ip(req).map_err(|_| ProxyError)?;
76 let secure = self.is_https(req).map_err(|_| ProxyError)?;
77 Ok(ProxyContext {
78 client_ip,
79 scheme: if secure { "https" } else { "http" },
80 })
81 }
82
83 pub(crate) fn accepts(&self, req: &Request) -> bool {
84 self.all || peer_ip(req).is_some_and(|ip| self.peers.contains(&ip))
85 }
86
87 pub(crate) fn client_ip(&self, req: &Request) -> ClientIp {
88 if !self.accepts(req) {
89 return Ok(peer_ip(req));
90 }
91 let Some(value) = single_header(req, "x-forwarded-for")? else {
92 return Ok(peer_ip(req));
93 };
94 let value = if self.all {
97 value.split(',').next().ok_or(())?
98 } else {
99 if value.contains(',') {
100 return Err(());
101 }
102 value
103 };
104 value
105 .trim()
106 .parse::<IpAddr>()
107 .map(|ip| Some(ip.to_canonical()))
108 .map_err(|_| ())
109 }
110
111 pub(crate) fn is_https(&self, req: &Request) -> Result<bool, ()> {
112 if req.uri().scheme_str() == Some("https") {
113 return Ok(true);
114 }
115 if !self.accepts(req) {
116 return Ok(false);
117 }
118 match single_header(req, "x-forwarded-proto")? {
119 None => Ok(false),
120 Some(value) if value.eq_ignore_ascii_case("https") => Ok(true),
121 Some(value) if value.eq_ignore_ascii_case("http") => Ok(false),
122 Some(_) => Err(()),
123 }
124 }
125}
126
127fn peer_ip(req: &Request) -> Option<IpAddr> {
128 req.extensions()
129 .get::<axum::extract::ConnectInfo<SocketAddr>>()
130 .map(|info| info.0.ip().to_canonical())
131}
132
133fn single_header<'a>(req: &'a Request, name: &str) -> Header<'a> {
134 let mut values = req.headers().get_all(name).iter();
135 let first = values
136 .next()
137 .map(|value| value.to_str().map_err(|_| ()))
138 .transpose()?;
139 if values.next().is_some() {
140 return Err(());
141 }
142 Ok(first)
143}