spark_server/rate_limiter/
identity.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! Who a request is, for rate-limiting purposes.
3//!
4//! Split out of `rate_limiter.rs`: identity resolution answers "who is this",
5//! the parent answers "how fast may they go", and this stack pushed the file
6//! from 457 to 507 lines against a 500 cap. A real separation rather than a
7//! line-count trick — byte-exact move.
8
9/// Resolve a stable identity from the request headers + peer addr. Used by
10/// the axum middleware; exposed here so tests can reuse it.
11pub fn extract_identity(
12    headers: &axum::http::HeaderMap,
13    peer: Option<std::net::SocketAddr>,
14) -> String {
15    use axum::http::header;
16    // 1. Bearer token.
17    if let Some(v) = headers
18        .get(header::AUTHORIZATION)
19        .and_then(|v| v.to_str().ok())
20        && let Some(tok) = v.strip_prefix("Bearer ")
21    {
22        let tok = tok.trim();
23        if !tok.is_empty() {
24            // Hash the token so we don't retain sensitive data in the map
25            // keys or Prometheus labels (if ever exposed).
26            return format!("bearer:{}", hash_token(tok));
27        }
28    }
29    // 2. X-Forwarded-For.
30    if let Some(xff) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok())
31        && let Some(first) = xff.split(',').next()
32    {
33        let first = first.trim();
34        if !first.is_empty() {
35            return format!("xff:{first}");
36        }
37    }
38    // 3. Peer socket.
39    match peer {
40        Some(addr) => format!("peer:{}", addr.ip()),
41        None => "peer:unknown".to_string(),
42    }
43}
44
45/// FNV-1a 64-bit hash. Avoids pulling a crypto dep; unnecessary here since
46/// we only need a stable opaque label, not collision resistance.
47fn hash_token(tok: &str) -> String {
48    let mut h: u64 = 0xcbf29ce484222325;
49    for b in tok.as_bytes() {
50        h ^= *b as u64;
51        h = h.wrapping_mul(0x100000001b3);
52    }
53    format!("{h:016x}")
54}