Files
ActivePush/src/lib.rs
T
2026-11-08 12:57:19 +01:00

116 lines
3.7 KiB
Rust

//! 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<app::App>) -> 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<String> = std::sync::OnceLock::new();
let csp = CSP.get_or_init(|| {
use base64::Engine;
use sha2::{Digest, Sha256};
let css = LANDING
.split_once("<style>")
.and_then(|(_, r)| r.split_once("</style>"))
.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<Arc<app::App>>,
) -> 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",
),
}
}