routes.rs 145 lignes · 5935 octets
//! Builder for the federation Axum router.
//!
//! Assembles the four discovery routes (`webfinger`, `nodeinfo` discovery,
//! `nodeinfo` detail, actor) into a single `Router` ready to be merged into
//! the application by the app entry point.

use std::sync::Arc;

use axum::{
    Extension, Router,
    extract::DefaultBodyLimit,
    routing::{get, post},
};
use sea_orm::DatabaseConnection;

/// Maximum inbound activity body size accepted on `/inbox` (ANSSI F-NET-1).
///
/// 1 MiB matches Mastodon's hard-coded ceiling for inbox activities.
/// Anything larger is rejected with 413 before deserialization, capping
/// the DoS surface from oversized JSON-LD payloads.
const INBOX_BODY_LIMIT_BYTES: usize = 1024 * 1024;

use crate::config::FediverseConfig;
use crate::services::fediverse_signal_service::{AlwaysAcceptVerifier, SignatureVerifier};
use rustwarden_core::config::auth::AuthConfig;

use super::actor;
use super::auth_routes::{self, AuthFlowState};
use super::inbox::{self, InboxState};
use super::invite_routes::{self, InviteRoutesState};
use super::nodeinfo;
use super::resolver::{LocalUserResolver, SeaOrmLocalUserResolver};
use super::state::FediverseState;
use super::webfinger;

/// Build the federation router with the default SeaORM-backed resolver
/// and the test-only [`AlwaysAcceptVerifier`].
///
/// **DEV/TEST ONLY** — ANSSI F-NET-5: this entry point uses
/// `AlwaysAcceptVerifier` which accepts any HTTP signature without
/// verifying it. Production callers must use
/// [`router_with_auth`] with a real verifier (`HttpSigVerifier`).
#[deprecated(
    since = "0.2.0",
    note = "use `router_with_auth` with an explicit `AuthConfig` and a production `HttpSigVerifier`"
)]
pub fn router(config: FediverseConfig, db: DatabaseConnection) -> Router<DatabaseConnection> {
    let resolver: Arc<dyn LocalUserResolver> = Arc::new(SeaOrmLocalUserResolver::new(db.clone()));
    // Debug/dev verifier with cache-population side-effect so the
    // testbed (FEDIVERSE_DEBUG_LOCAL_FEDERATION=true) doesn't need to
    // manually pre-seed `remote_identities`.
    let verifier: Arc<dyn SignatureVerifier> = Arc::new(AlwaysAcceptVerifier::with_resolver(db));
    router_with(config, resolver, verifier)
}

/// Build the federation router with custom resolver and signature
/// verifier implementations. Used by integration tests, consumers that
/// already maintain their own user store, or apps wanting to plug a
/// production HTTP-Sig verifier (RFC 0012).
pub fn router_with(
    config: FediverseConfig,
    resolver: Arc<dyn LocalUserResolver>,
    verifier: Arc<dyn SignatureVerifier>,
) -> Router<DatabaseConnection> {
    let challenge_ttl = config.auth_challenge_ttl();
    let state = FediverseState { config, resolver };
    let inbox_state = InboxState {
        verifier: verifier.clone(),
    };
    // ANSSI F-NET-5 — fail loudly when env vars are missing rather
    // than silently falling back to a placeholder JWT secret. Production
    // callers should prefer `router_with_auth` which takes
    // `AuthConfig` explicitly and lets the caller surface configuration
    // errors as proper startup failures.
    let auth_flow_state = AuthFlowState {
        auth_config: Arc::new(AuthConfig::from_env().expect(
            "AuthConfig::from_env() failed — set JWT_SECRET and related env vars, \
             or call `router_with_auth` with an explicit AuthConfig",
        )),
        verifier,
        challenge_ttl,
    };
    Router::new()
        .route("/.well-known/webfinger", get(webfinger::handle))
        .route("/.well-known/nodeinfo", get(nodeinfo::discovery))
        .route("/nodeinfo/2.1", get(nodeinfo::detail))
        .route("/users/{username}", get(actor::handle))
        .route(
            "/inbox",
            post(inbox::handle).layer(DefaultBodyLimit::max(INBOX_BODY_LIMIT_BYTES)),
        )
        .merge(auth_routes::router(auth_flow_state))
        .merge(invite_routes::router(InviteRoutesState {
            default_ttl: std::time::Duration::from_secs(7 * 86400),
        }))
        .layer(Extension(state))
        .layer(Extension(inbox_state))
}

/// Build the federation router with explicit `AuthConfig` injection.
/// Use this entry point when the consumer cannot rely on env vars
/// being set at router-build time.
pub fn router_with_auth(
    config: FediverseConfig,
    resolver: Arc<dyn LocalUserResolver>,
    verifier: Arc<dyn SignatureVerifier>,
    auth_config: Arc<AuthConfig>,
) -> Router<DatabaseConnection> {
    let challenge_ttl = config.auth_challenge_ttl();
    let state = FediverseState { config, resolver };
    let inbox_state = InboxState {
        verifier: verifier.clone(),
    };
    let auth_flow_state = AuthFlowState {
        auth_config,
        verifier,
        challenge_ttl,
    };
    Router::new()
        .route("/.well-known/webfinger", get(webfinger::handle))
        .route("/.well-known/nodeinfo", get(nodeinfo::discovery))
        .route("/nodeinfo/2.1", get(nodeinfo::detail))
        .route("/users/{username}", get(actor::handle))
        .route(
            "/inbox",
            post(inbox::handle).layer(DefaultBodyLimit::max(INBOX_BODY_LIMIT_BYTES)),
        )
        .merge(auth_routes::router(auth_flow_state))
        .merge(invite_routes::router(InviteRoutesState {
            default_ttl: std::time::Duration::from_secs(7 * 86400),
        }))
        .layer(Extension(state))
        .layer(Extension(inbox_state))
}

/// Backwards-compatible alias for [`router_with`] kept until the next
/// major version; new code should call `router_with` directly.
#[deprecated(since = "0.2.0", note = "use `router_with` (RFC 0012)")]
pub fn router_with_resolver(
    config: FediverseConfig,
    resolver: Arc<dyn LocalUserResolver>,
) -> Router<DatabaseConnection> {
    let verifier: Arc<dyn SignatureVerifier> = Arc::new(AlwaysAcceptVerifier::new());
    router_with(config, resolver, verifier)
}