use std::collections::HashMap;
use base64::engine::{Engine as _, general_purpose::STANDARD as BASE64};
use subtle::ConstantTimeEq;
struct UserEntry {
encoded: Vec<u8>,
name: String,
}
pub struct UserLookup {
lookup: HashMap<[u8; 32], usize>,
users: Vec<UserEntry>,
}
impl std::fmt::Debug for UserLookup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UserLookup")
.field("num_users", &self.users.len())
.finish()
}
}
impl UserLookup {
pub fn new(credentials: Vec<(String, String, String)>) -> Self {
assert!(
!credentials.is_empty(),
"NaiveProxy requires at least one user"
);
let mut lookup = HashMap::with_capacity(credentials.len());
let mut users = Vec::with_capacity(credentials.len());
for (i, (name, username, password)) in credentials.into_iter().enumerate() {
let cred_string = format!("{}:{}", username, password);
let encoded = BASE64.encode(&cred_string).into_bytes();
let hash = blake3::hash(&encoded);
lookup.insert(*hash.as_bytes(), i);
users.push(UserEntry { encoded, name });
}
Self { lookup, users }
}
pub fn validate(&self, auth_header: &str) -> Option<&str> {
let encoded = auth_header.strip_prefix("Basic ")?.as_bytes();
let hash = blake3::hash(encoded);
let idx = self.lookup.get(hash.as_bytes())?;
let user = &self.users[*idx];
if user.encoded.ct_eq(encoded).unwrap_u8() == 1 {
Some(&user.name)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_lookup_basic() {
let lookup = UserLookup::new(vec![(
"alice".to_string(),
"user".to_string(),
"pass".to_string(),
)]);
assert_eq!(lookup.validate("Basic dXNlcjpwYXNz"), Some("alice"));
assert_eq!(lookup.users.len(), 1);
}
#[test]
fn test_user_lookup_special_chars() {
let lookup = UserLookup::new(vec![(
"bob".to_string(),
"user".to_string(),
"p@ss:w0rd!".to_string(),
)]);
let encoded = BASE64.encode("user:p@ss:w0rd!");
let header = format!("Basic {}", encoded);
assert_eq!(lookup.validate(&header), Some("bob"));
}
#[test]
fn test_user_lookup_empty_password() {
let lookup = UserLookup::new(vec![(
"test".to_string(),
"user".to_string(),
"".to_string(),
)]);
let encoded = BASE64.encode("user:");
let header = format!("Basic {}", encoded);
assert_eq!(lookup.validate(&header), Some("test"));
}
#[test]
fn test_user_lookup_invalid_credentials() {
let lookup = UserLookup::new(vec![(
"alice".to_string(),
"user".to_string(),
"pass".to_string(),
)]);
assert_eq!(lookup.validate("Basic invalid"), None);
assert_eq!(lookup.validate("Basic d3Jvbmc6cGFzcw=="), None); assert_eq!(lookup.validate("Bearer token"), None);
assert_eq!(lookup.validate(""), None);
}
#[test]
fn test_user_lookup_multiple_users() {
let lookup = UserLookup::new(vec![
(
"alice".to_string(),
"alice".to_string(),
"alice123".to_string(),
),
("bob".to_string(), "bob".to_string(), "bob456".to_string()),
(
"charlie".to_string(),
"charlie".to_string(),
"charlie789".to_string(),
),
]);
assert_eq!(lookup.users.len(), 3);
let alice_header = format!("Basic {}", BASE64.encode("alice:alice123"));
let bob_header = format!("Basic {}", BASE64.encode("bob:bob456"));
let charlie_header = format!("Basic {}", BASE64.encode("charlie:charlie789"));
assert_eq!(lookup.validate(&alice_header), Some("alice"));
assert_eq!(lookup.validate(&bob_header), Some("bob"));
assert_eq!(lookup.validate(&charlie_header), Some("charlie"));
}
}