use axum::{
extract::FromRequestParts,
http::{request::Parts, HeaderMap, StatusCode},
Json,
};
use std::sync::Arc;
use crate::hub::HubState;
pub const UNKNOWN_VTOKEN_MSG: &str = "Unknown or revoked virtual token; register via POST /hub/register or ilink-hub-bridge --force-register";
pub(crate) fn is_valid_vtoken(s: &str) -> bool {
let Some(rest) = s.strip_prefix("vhub_") else {
return false;
};
rest.len() == 32
&& rest
.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
}
pub fn extract_vtoken_pub(headers: &axum::http::HeaderMap) -> Option<String> {
extract_vtoken(headers)
}
pub(super) fn extract_vtoken(headers: &axum::http::HeaderMap) -> Option<String> {
headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.filter(|s| is_valid_vtoken(s))
.map(crate::hub::hash_vtoken)
}
pub(super) fn check_admin_auth(admin: &crate::hub::AdminConfig, headers: &HeaderMap) -> bool {
if let Some(required) = &admin.token {
let provided = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.unwrap_or("");
use subtle::ConstantTimeEq;
return provided.as_bytes().ct_eq(required.as_bytes()).unwrap_u8() == 1;
}
admin.insecure_no_auth
}
pub struct AdminGuard;
impl FromRequestParts<Arc<HubState>> for AdminGuard {
type Rejection = (StatusCode, Json<serde_json::Value>);
async fn from_request_parts(
parts: &mut Parts,
state: &Arc<HubState>,
) -> Result<Self, Self::Rejection> {
let headers = &parts.headers;
if check_admin_auth(&state.admin, headers) {
Ok(AdminGuard)
} else {
Err((
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"error": "Unauthorized",
"ret": 401,
"errmsg": "Unauthorized",
})),
))
}
}
}