#[must_use]
pub fn build_rate_limit_key(strategy: &str, identifier: &str, prefix: Option<&str>) -> String {
match prefix {
Some(p) => format!("fraiseql:rl:{strategy}:{p}:{identifier}"),
None => format!("fraiseql:rl:{strategy}:{identifier}"),
}
}
pub(super) const fn is_private_or_loopback(ip: std::net::IpAddr) -> bool {
match ip {
std::net::IpAddr::V4(v4) => v4.is_loopback() || v4.is_private() || v4.is_link_local(),
std::net::IpAddr::V6(v6) => v6.is_loopback(),
}
}
pub(super) fn path_matches_rule(path: &str, prefix: &str) -> bool {
if path == prefix {
return true;
}
let Some(rest) = path.strip_prefix(prefix) else {
return false;
};
rest.starts_with('/') || rest.starts_with('?')
}
#[derive(Debug, Clone)]
pub(super) struct PathRateLimit {
pub(super) path_prefix: String,
pub(super) tokens_per_sec: f64,
pub(super) burst: f64,
}
impl PathRateLimit {
#[allow(clippy::cast_precision_loss)] fn rule(prefix: &str, max_requests: u32, window_secs: u64) -> Option<Self> {
(max_requests > 0 && window_secs > 0).then(|| Self {
path_prefix: prefix.to_string(),
tokens_per_sec: f64::from(max_requests) / window_secs as f64,
burst: f64::from(max_requests),
})
}
pub(super) fn rules_from_security(
sec: &super::config::RateLimitingSecurityConfig,
) -> Vec<Self> {
[
Self::rule("/auth/start", sec.auth_start_max_requests, sec.auth_start_window_secs),
Self::rule(
"/auth/v1/authorize",
sec.auth_start_max_requests,
sec.auth_start_window_secs,
),
Self::rule(
"/auth/callback",
sec.auth_callback_max_requests,
sec.auth_callback_window_secs,
),
Self::rule(
"/auth/v1/callback",
sec.auth_callback_max_requests,
sec.auth_callback_window_secs,
),
Self::rule(
"/auth/refresh",
sec.auth_refresh_max_requests,
sec.auth_refresh_window_secs,
),
Self::rule("/auth/logout", sec.auth_logout_max_requests, sec.auth_logout_window_secs),
]
.into_iter()
.flatten()
.collect()
}
}
pub(super) fn normalise_ip_key(candidate: &str) -> Option<String> {
use std::net::IpAddr;
match candidate.trim().parse::<IpAddr>().ok()? {
IpAddr::V4(v4) => Some(v4.to_string()),
IpAddr::V6(v6) => {
let mut octets = v6.octets();
octets[8..].fill(0);
Some(format!("{}/64", std::net::Ipv6Addr::from(octets)))
},
}
}