main.rs 173 lignes · 9066 octets
//! Reference application for the Rustwarden framework.

use rustwarden_core::prelude::*;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let builder = RustwardenApp::builder()
        .from_env()
        // Browser SSR routes go through the CSRF-protected channel; the
        // `/api/v1/*` JSON API is mounted on the machine channel (no CSRF).
        .with_ui(rustwarden_ui::browser_routes)
        .merge_machine_routes(rustwarden_ui::api_routes());

    // ActivityPub federation lives in the `rustwarden-fediverse` crate
    // (cf. docs/plans/2026-05-11-extract-rustwarden-fediverse-crate.md).
    // Enabling the `fediverse` feature links the crate and mounts its
    // routes onto the application router. The wiring opens a database
    // connection early so the federation router and the revocation
    // checker share the same `DatabaseConnection` as the rest of the
    // framework.
    #[cfg(feature = "fediverse")]
    let builder = {
        use std::sync::Arc;
        let db_url = std::env::var("DATABASE_URL")
            .map_err(|_| anyhow::anyhow!("DATABASE_URL must be set for the fediverse feature"))?;
        let db = sea_orm::Database::connect(&db_url).await?;
        let fediverse_config = rustwarden_fediverse::FediverseConfig::from_env()
            .map_err(|e| anyhow::anyhow!("invalid FediverseConfig: {e}"))?;

        // ANSSI F-NET-5 — load AuthConfig explicitly so misconfiguration
        // surfaces as a startup error rather than silently booting with
        // a placeholder JWT secret.
        let auth_config: Arc<rustwarden_core::config::auth::AuthConfig> = Arc::new(
            rustwarden_core::config::auth::AuthConfig::from_env()
                .map_err(|e| anyhow::anyhow!("invalid AuthConfig for fediverse: {e}"))?,
        );

        // ANSSI F-CRYPTO-1/2 — production wires the strict
        // `HttpSigVerifier`, which enforces the Digest header and the
        // 5-min Date skew window. Dev / testbed mode (when
        // `FEDIVERSE_DEBUG_LOCAL_FEDERATION=true`) falls back to
        // `AlwaysAcceptVerifier::with_resolver` so the e2e suite can
        // exercise the signed-request happy path without the
        // testbed pre-seeding every actor's PEM into each peer's cache.
        let resolver: Arc<dyn rustwarden_fediverse::resolver::LocalUserResolver> =
            Arc::new(rustwarden_fediverse::resolver::SeaOrmLocalUserResolver::new(db.clone()));
        let debug_local = std::env::var("FEDIVERSE_DEBUG_LOCAL_FEDERATION")
            .map(|v| matches!(v.to_ascii_lowercase().as_str(), "true" | "1" | "yes"))
            .unwrap_or(false);
        let verifier: Arc<
            dyn rustwarden_fediverse::services::fediverse_signal_service::SignatureVerifier,
        > = if debug_local {
            tracing::warn!(
                "FEDIVERSE_DEBUG_LOCAL_FEDERATION=true — using AlwaysAcceptVerifier (DEV ONLY)"
            );
            Arc::new(
                rustwarden_fediverse::services::fediverse_signal_service::AlwaysAcceptVerifier::with_resolver(
                    db.clone(),
                ),
            )
        } else {
            Arc::new(
                rustwarden_fediverse::http_signatures::HttpSigVerifier::with_resolver(db.clone()),
            )
        };
        let fediverse_router = rustwarden_fediverse::routes::router_with_auth(
            fediverse_config,
            resolver,
            verifier,
            auth_config,
        );

        // RFC 0028 / Lot 7d — branch the guard observer on the
        // fediverse router when the `fediverse-guard` feature is active.
        // The observer extension layer is read by inbox / auth_routes /
        // signature verifier; absent layer ⇒ everything defaults to
        // `NoopFediverseObserver` (zero-cost no-op).
        //
        // Lot 2 CSRF — the guard observer stays bolted onto the machine
        // `fediverse_router` (it serves the inbox), but the guard admin UI
        // is now split out into its own `guard_ui_router` so it can ride the
        // CSRF-protected browser channel (see below). The store and config
        // are still shared between the runtime ban-manager and the UI so a
        // single source of truth backs both.
        #[cfg(feature = "fediverse-guard")]
        let (fediverse_router, guard_ui_router) = {
            use rustwarden_fediverse::observer::FediverseEventObserver;
            use rustwarden_fediverse_guard::{
                BanManager, GuardConfig, GuardObserver, GuardStore, HybridStore, PostgresStore,
                default_detectors,
            };

            let guard_config =
                GuardConfig::from_env().map_err(|e| anyhow::anyhow!("invalid GuardConfig: {e}"))?;
            let postgres_store: Arc<dyn GuardStore> = Arc::new(PostgresStore::new(db.clone()));
            let hybrid = HybridStore::new(postgres_store);
            // Repopulate the in-memory cache from durable storage before
            // serving requests — bans active at boot must be enforced
            // immediately, not only after the first detector fires.
            if let Err(e) = hybrid.rehydrate().await {
                tracing::warn!(error = %e, "fediverse guard rehydrate failed; continuing with empty cache");
            }
            let hybrid_store: Arc<dyn GuardStore> = Arc::new(hybrid);
            let manager = BanManager::new(hybrid_store.clone(), guard_config.clone());
            let detectors = default_detectors(&guard_config);
            let observer: Arc<dyn FediverseEventObserver> =
                Arc::new(GuardObserver::new(manager, detectors, None));
            // Machine router: keep the inbox + observer here. The observer
            // extension is read on the inbox path.
            let fediverse_router = fediverse_router.layer(axum::Extension(observer));
            // Browser router: the guard admin UI. Its single POST
            // (`/admin/fediverse-guard/manual_ban`) is a urlencoded form
            // carrying a hidden `csrf_token`; the handler still extracts
            // `Form { csrf_token }` and calls `validate_csrf_token`
            // (defense-in-depth, D3=a), while the global browser CSRF layer
            // validates the same token from the buffered body and rebuilds
            // it for the downstream extractor (cf. core test T4).
            let guard_ui_router = rustwarden_fediverse_guard::ui::default_routes()
                .layer(axum::Extension(hybrid_store))
                .layer(axum::Extension(guard_config));
            (fediverse_router, guard_ui_router)
        };

        let checker: Arc<
            dyn rustwarden_core::middleware::revocation::RemoteIdentityRevocationChecker,
        > = Arc::new(
            rustwarden_fediverse::services::revocation::SeaOrmRemoteIdentityRevocationChecker::new(
                db.clone(),
            ),
        );
        // The fediverse router carries `POST /inbox` (machine, authenticated
        // by HTTP signatures — no CSRF token possible), so it goes on the
        // machine channel.
        let builder = builder
            .database(db)
            .register_protected_prefix("/fediverse/consent")
            .register_protected_prefix("/fediverse/invite")
            .register_revocation_checker(checker)
            .merge_machine_routes(fediverse_router);

        // The guard admin UI is a browser family (SSR forms under
        // `/admin/fediverse-guard/*`): mount it on the CSRF-protected
        // browser channel and register its prefix so the auth middleware
        // gates it like every other `/admin/*` screen.
        #[cfg(feature = "fediverse-guard")]
        let builder = builder
            .register_protected_prefix("/admin/fediverse-guard")
            .merge_browser_routes(guard_ui_router)
            .add_dashboard_card(rustwarden_fediverse_guard::ui::dashboard_card());

        builder
    };

    // The reference UI for the federation lives in
    // `rustwarden-fediverse-ui` and is opt-in via the `fediverse-ui`
    // feature (which itself implies `fediverse`). When enabled, mount
    // the UI router and inject the federation card into the admin
    // dashboard via the builder hook.
    #[cfg(feature = "fediverse-ui")]
    let builder = builder
        // Famille browser : login fediverse, consent, invite et écrans
        // admin sont des formulaires SSR pilotés par cookie de session, on
        // les monte donc sur le canal CSRF-protégé. Chaque handler conserve
        // son `validate_csrf_token` en defense-in-depth (D3=a) ; le layer
        // CSRF global lit le token dans le body urlencoded bufferisé puis
        // reconstruit le body pour l'extracteur `Form<…>` aval (réf. test
        // core T4). L'inbox ActivityPub n'est PAS ici : elle reste sur le
        // canal machine via `fediverse_router`.
        .merge_browser_routes(rustwarden_fediverse_ui::default_routes())
        .add_dashboard_card(rustwarden_fediverse_ui::dashboard_card());

    builder.build().await?.run().await
}