use axum::extract::{ConnectInfo, Request};
use ipnet::IpNet;
use std::net::SocketAddr;
use std::sync::Arc;
use systemprompt_models::RequestContext;
use systemprompt_models::auth::UserType;
use crate::services::middleware::client_addr::{forwarded_headers_ignored, resolve_client_ip};
const UNTRUSTED_PROXY_WARN_INTERVAL_SECS: u64 = 300;
#[derive(Clone, Debug)]
pub struct IdentityOrTrustedIpKey {
trusted_proxies: Arc<Vec<IpNet>>,
untrusted_proxy_warn: Arc<systemprompt_logging::LogThrottle>,
}
impl IdentityOrTrustedIpKey {
pub(super) fn new(trusted_proxies: Arc<Vec<IpNet>>) -> Self {
Self {
trusted_proxies,
untrusted_proxy_warn: Arc::new(systemprompt_logging::LogThrottle::new(
UNTRUSTED_PROXY_WARN_INTERVAL_SECS,
)),
}
}
fn warn_if_proxy_untrusted<T>(&self, req: &Request<T>) {
let Some(peer) = req.extensions().get::<ConnectInfo<SocketAddr>>() else {
return;
};
if forwarded_headers_ignored(req.headers(), peer.0.ip(), &self.trusted_proxies)
&& self.untrusted_proxy_warn.allow()
{
tracing::warn!(
peer_ip = %peer.0.ip(),
"rate limiting by proxy address: this request carried forwarded client-IP \
headers but the peer is absent from server.trusted_proxies, so every client \
behind that proxy shares one rate-limit bucket and will exhaust it together; \
add the peer's range to server.trusted_proxies"
);
}
}
}
impl tower_governor::key_extractor::KeyExtractor for IdentityOrTrustedIpKey {
type Key = String;
fn extract<T>(&self, req: &Request<T>) -> Result<Self::Key, tower_governor::GovernorError> {
if let Some(ctx) = req.extensions().get::<RequestContext>()
&& ctx.auth.user_type != UserType::Anon
{
return Ok(format!("u:{}", ctx.user_id()));
}
self.warn_if_proxy_untrusted(req);
resolve_client_ip(
req.headers(),
req.extensions().get::<ConnectInfo<SocketAddr>>(),
&self.trusted_proxies,
)
.map(|ip| format!("ip:{ip}"))
.ok_or(tower_governor::GovernorError::UnableToExtractKey)
}
}