use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
pub(super) const PREFIX: &str = "/_verify";
const LIFETIME: Duration = Duration::from_secs(60);
type Minted = (String, String, Option<String>, Instant);
static TOKENS: Mutex<Option<HashMap<String, Minted>>> = Mutex::new(None);
pub(super) fn mint(owner: &str, project: &str, section: Option<&str>) -> String {
let token = uuid::Uuid::new_v4().simple().to_string();
let now = Instant::now();
let mut guard = TOKENS.lock().unwrap_or_else(|e| e.into_inner());
let tokens = guard.get_or_insert_with(HashMap::new);
tokens.retain(|_, (_, _, _, at)| now.duration_since(*at) < LIFETIME);
tokens.insert(token.clone(), (owner.to_string(), project.to_string(), section.map(str::to_string), now));
token
}
pub(super) fn answer(path: &str, owner: &str, project: &str, section: Option<&str>) -> Response {
let rest = path.strip_prefix(PREFIX).unwrap_or_default().trim_matches('/');
if rest.is_empty() {
return StatusCode::NO_CONTENT.into_response();
}
let mut guard = TOKENS.lock().unwrap_or_else(|e| e.into_inner());
let found = guard.as_mut().and_then(|tokens| tokens.remove(rest));
match found {
Some((o, p, s, at)) if o == owner && p == project && s.as_deref() == section && at.elapsed() < LIFETIME => {
StatusCode::NO_CONTENT.into_response()
}
_ => {
let mut resp = StatusCode::NOT_FOUND.into_response();
resp.headers_mut().insert("x-zebflow-verify", HeaderValue::from_static("other"));
resp
}
}
}
pub(super) fn is_probe(path: &str) -> bool {
path == PREFIX || path.starts_with(&format!("{PREFIX}/"))
}
#[cfg(test)]
mod tests {
use axum::http::StatusCode;
use super::{answer, mint};
#[test]
fn a_token_proves_its_own_project_once() {
let token = mint("site-a", "demo", None);
assert_eq!(answer(&format!("/_verify/{token}"), "site-a", "catalog", None).status(), StatusCode::NOT_FOUND, "another project");
let token = mint("site-a", "demo", None);
assert_eq!(answer(&format!("/_verify/{token}"), "site-a", "demo", None).status(), StatusCode::NO_CONTENT);
assert_eq!(answer(&format!("/_verify/{token}"), "site-a", "demo", None).status(), StatusCode::NOT_FOUND, "used up");
assert_eq!(answer("/_verify", "site-a", "demo", None).status(), StatusCode::NO_CONTENT);
assert_eq!(answer("/_verify/unknown", "site-a", "demo", None).status(), StatusCode::NOT_FOUND);
let token = mint("site-a", "demo", Some("shop"));
assert_eq!(answer(&format!("/_verify/{token}"), "site-a", "demo", Some("clinic")).status(), StatusCode::NOT_FOUND, "another section");
let token = mint("site-a", "demo", Some("shop"));
assert_eq!(answer(&format!("/_verify/{token}"), "site-a", "demo", None).status(), StatusCode::NOT_FOUND, "the rootless host");
let token = mint("site-a", "demo", Some("shop"));
assert_eq!(answer(&format!("/_verify/{token}"), "site-a", "demo", Some("shop")).status(), StatusCode::NO_CONTENT);
}
}