116 lines
3.7 KiB
Rust
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",
|
|
),
|
|
}
|
|
}
|