mail4agent-server 0.3.0

Matrix-subset homeserver: protocol decisions plus the Client-Server HTTP routes. No users database, logins, registration or tariffs: identities are nick + domain; a product server vouches for callers through the signed-assertion seam (m4a-seam), lifecycle events and the policy hook.
Documentation
//! Core-side gate for the edge/core split. When the server runs with
//! `--role core`, every request must carry the shared secret the edge adds
//! (`X-M4A-Edge-Secret`). The tunnel and a firewall allowlist are the first
//! line; this header is the second, so a stray process on the tunnel network
//! cannot talk to the core. The secret comes from the environment and is never
//! logged.

use axum::extract::Request;
use axum::http::StatusCode;
use axum::middleware::{from_fn, Next};
use axum::response::IntoResponse;
use axum::Router;

/// Header name carrying the shared secret edge -> core.
pub const EDGE_SECRET_HEADER: &str = "x-m4a-edge-secret";

fn ct_eq(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
}

/// Wraps `router` so requests without the right secret get a Matrix-shaped 401.
pub fn require_edge_secret(router: Router, secret: String) -> Router {
    let secret: std::sync::Arc<str> = secret.into();
    router.layer(from_fn(move |req: Request, next: Next| {
        let secret = std::sync::Arc::clone(&secret);
        async move {
            let ok = req
                .headers()
                .get(EDGE_SECRET_HEADER)
                .and_then(|v| v.to_str().ok())
                .is_some_and(|got| ct_eq(got.as_bytes(), secret.as_bytes()));
            if ok {
                next.run(req).await
            } else {
                (StatusCode::UNAUTHORIZED, axum::Json(serde_json::json!({"errcode": "M_UNKNOWN_TOKEN", "error": "edge secret required"}))).into_response()
            }
        }
    }))
}

/// Barrier token gate for a standalone core reached directly by a product server
/// (`M4A_LINK_TOKEN`): every request outside the public protocol surfaces must carry
/// `x-m4a-link-token`. The header is removed before routing.
pub fn require_link_token(router: Router, token: String) -> Router {
    let token: std::sync::Arc<str> = token.into();
    router.layer(from_fn(move |mut req: Request, next: Next| {
        let token = std::sync::Arc::clone(&token);
        async move {
            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);
            req.headers_mut().remove(m4a_seam::LINK_TOKEN_HEADER);
            if ok {
                next.run(req).await
            } else {
                (StatusCode::UNAUTHORIZED, axum::Json(serde_json::json!({"errcode": "M4A_LINK_TOKEN_REQUIRED", "error": "link token required"}))).into_response()
            }
        }
    }))
}

#[cfg(test)]
mod tests {
    #[test]
    fn constant_time_compare() {
        assert!(super::ct_eq(b"abc", b"abc"));
        assert!(!super::ct_eq(b"abc", b"abd"));
        assert!(!super::ct_eq(b"abc", b"ab"));
    }
}