use std::net::IpAddr;
use std::sync::Arc;
use axum::http::header;
use axum::http::request::Parts;
use axum::http::{HeaderMap, Request};
use hmac::Mac;
use crate::authn::ids::TenantId;
use crate::device::types::FingerprintHash;
pub trait DeviceFingerprintExtractor: Send + Sync + 'static {
fn extract(
&self,
tenant_id: &TenantId,
parts: &Parts,
client_ip: Option<IpAddr>,
) -> Option<FingerprintHash>;
fn extract_from_request<B>(
&self,
tenant_id: &TenantId,
request: &Request<B>,
client_ip: Option<IpAddr>,
) -> Option<FingerprintHash> {
let placeholder: Request<()> = Request::new(());
let (mut synthetic, _) = placeholder.into_parts();
synthetic.headers = request.headers().clone();
synthetic.uri = request.uri().clone();
synthetic.method = request.method().clone();
synthetic.version = request.version();
self.extract(tenant_id, &synthetic, client_ip)
}
}
pub type TenantPepperResolver = Arc<dyn Fn(&TenantId) -> [u8; 32] + Send + Sync>;
pub struct DefaultFingerprintExtractor {
pepper: TenantPepperResolver,
}
impl DefaultFingerprintExtractor {
pub fn new(pepper: TenantPepperResolver) -> Self {
Self { pepper }
}
}
impl DeviceFingerprintExtractor for DefaultFingerprintExtractor {
fn extract(
&self,
tenant_id: &TenantId,
parts: &Parts,
client_ip: Option<IpAddr>,
) -> Option<FingerprintHash> {
let headers = &parts.headers;
let ua = headers.get(header::USER_AGENT)?;
let pepper = (self.pepper)(tenant_id);
let mut mac = crate::hmac::new_signer(&pepper);
mac.update(b"axess.device.v1\0");
mac.update(b"ua\0");
mac.update(ua.as_bytes());
mac.update(b"\0");
if let Some(top) = accept_language_top(headers) {
mac.update(b"al\0");
mac.update(top.as_bytes());
mac.update(b"\0");
}
if let Some(ip) = client_ip {
mac.update(b"ip\0");
mac.update(&truncate_ip(ip));
mac.update(b"\0");
}
let bytes: [u8; 32] = mac.finalize().into_bytes().into();
Some(FingerprintHash::from_bytes(bytes))
}
}
fn accept_language_top(headers: &HeaderMap) -> Option<String> {
let raw = headers.get(header::ACCEPT_LANGUAGE)?.to_str().ok()?;
let head = raw.split(',').next()?.trim();
let head = head.split(';').next()?.trim();
if head.is_empty() {
return None;
}
Some(head.to_ascii_lowercase())
}
fn truncate_ip(ip: IpAddr) -> Vec<u8> {
match ip {
IpAddr::V4(v4) => {
let o = v4.octets();
vec![o[0], o[1], o[2], 0]
}
IpAddr::V6(v6) => {
let mut o = v6.octets();
for byte in &mut o[6..] {
*byte = 0;
}
o.to_vec()
}
}
}
#[cfg(test)]
mod tests;