mail4agent_server/http/
edge_auth.rs1use axum::extract::Request;
9use axum::http::StatusCode;
10use axum::middleware::{from_fn, Next};
11use axum::response::IntoResponse;
12use axum::Router;
13
14pub const EDGE_SECRET_HEADER: &str = "x-m4a-edge-secret";
16
17fn ct_eq(a: &[u8], b: &[u8]) -> bool {
18 if a.len() != b.len() {
19 return false;
20 }
21 a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
22}
23
24pub fn require_edge_secret(router: Router, secret: String) -> Router {
26 let secret: std::sync::Arc<str> = secret.into();
27 router.layer(from_fn(move |req: Request, next: Next| {
28 let secret = std::sync::Arc::clone(&secret);
29 async move {
30 let ok = req
31 .headers()
32 .get(EDGE_SECRET_HEADER)
33 .and_then(|v| v.to_str().ok())
34 .is_some_and(|got| ct_eq(got.as_bytes(), secret.as_bytes()));
35 if ok {
36 next.run(req).await
37 } else {
38 (StatusCode::UNAUTHORIZED, axum::Json(serde_json::json!({"errcode": "M_UNKNOWN_TOKEN", "error": "edge secret required"}))).into_response()
39 }
40 }
41 }))
42}
43
44pub fn require_link_token(router: Router, token: String) -> Router {
48 let token: std::sync::Arc<str> = token.into();
49 router.layer(from_fn(move |mut req: Request, next: Next| {
50 let token = std::sync::Arc::clone(&token);
51 async move {
52 let ok = m4a_seam::link_path_is_open(req.uri().path()) || m4a_seam::link_token_ok(req.headers().get(m4a_seam::LINK_TOKEN_HEADER).and_then(|v| v.to_str().ok()), &token);
53 req.headers_mut().remove(m4a_seam::LINK_TOKEN_HEADER);
54 if ok {
55 next.run(req).await
56 } else {
57 (StatusCode::UNAUTHORIZED, axum::Json(serde_json::json!({"errcode": "M4A_LINK_TOKEN_REQUIRED", "error": "link token required"}))).into_response()
58 }
59 }
60 }))
61}
62
63#[cfg(test)]
64mod tests {
65 #[test]
66 fn constant_time_compare() {
67 assert!(super::ct_eq(b"abc", b"abc"));
68 assert!(!super::ct_eq(b"abc", b"abd"));
69 assert!(!super::ct_eq(b"abc", b"ab"));
70 }
71}