//! ActivePush - Exchange ActiveSync gateway for IMAP/SMTP + CalDAV/CardDAV (Z-Push rewrite). pub mod admin; pub mod app; pub mod autodiag; pub mod autodiscover; pub mod backend; pub mod config; pub mod db; pub mod dnscheck; pub mod eas; pub mod logging; pub mod metrics; pub mod probe; pub mod server; pub mod tls; pub mod util; pub mod wbxml; pub mod xmltree; use std::sync::Arc; use axum::routing::any; use axum::Router; /// Public (device facing) router: ActiveSync + Autodiscover. Paths match the nginx /// locations previously routed to Z-Push. pub fn public_router(app: Arc) -> Router { Router::new() .route("/Microsoft-Server-ActiveSync", any(eas::handler)) .route("/microsoft-server-activesync", any(eas::handler)) .route("/AutoDiscover/AutoDiscover.xml", any(autodiscover::handler)) .route("/Autodiscover/Autodiscover.xml", any(autodiscover::handler)) .route("/autodiscover/autodiscover.xml", any(autodiscover::handler)) .route("/healthz", axum::routing::get(healthz)) .route("/", axum::routing::get(index)) .route("/favicon.svg", axum::routing::get(favicon)) .fallback(not_found) .layer(axum::middleware::map_response(mark)) .with_state(app) } /// Marker used by the panel diagnostics to tell ActivePush from another application behind the proxy. async fn mark(mut r: axum::response::Response) -> axum::response::Response { r.headers_mut() .entry("X-ActivePush") .or_insert(axum::http::HeaderValue::from_static("1")); r } const LANDING: &str = include_str!("../web/landing.html"); const FAVICON: &str = include_str!("../web/favicon.svg"); /// Landing page for browsers opening the ActiveSync host. Self-contained (inline SVG, one inline /// style block pinned by a hash in the CSP, no scripts, no external requests). async fn index() -> impl axum::response::IntoResponse { static CSP: std::sync::OnceLock = std::sync::OnceLock::new(); let csp = CSP.get_or_init(|| { use base64::Engine; use sha2::{Digest, Sha256}; let css = LANDING .split_once("")) .map(|(c, _)| c) .unwrap_or(""); let hash = base64::engine::general_purpose::STANDARD.encode(Sha256::digest(css.as_bytes())); format!( "default-src 'none'; style-src 'sha256-{hash}'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" ) }); ( [ ("Content-Type", "text/html; charset=utf-8"), ("Content-Security-Policy", csp.as_str()), ("X-Content-Type-Options", "nosniff"), ("Referrer-Policy", "no-referrer"), ("Cache-Control", "no-cache"), ], LANDING, ) } async fn favicon() -> impl axum::response::IntoResponse { ( [ ("Content-Type", "image/svg+xml"), ("Cache-Control", "public, max-age=86400"), ], FAVICON, ) } async fn not_found() -> impl axum::response::IntoResponse { ( axum::http::StatusCode::NOT_FOUND, [("Content-Type", "text/plain; charset=utf-8")], "Not found.\n", ) } /// Liveness + database check for load balancers and monitoring. async fn healthz( axum::extract::State(app): axum::extract::State>, ) -> impl axum::response::IntoResponse { match sqlx::query_scalar::<_, i64>("SELECT 1") .fetch_one(&app.db) .await { Ok(_) => (axum::http::StatusCode::OK, "ok\n"), Err(_) => ( axum::http::StatusCode::SERVICE_UNAVAILABLE, "database unavailable\n", ), } }