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}