Skip to main content

brainos_httpadapter/
server.rs

1//! HTTP server construction and lifecycle.
2
3use std::collections::HashMap;
4use std::{net::SocketAddr, sync::Arc};
5
6use axum::{routing::delete, Router};
7use channel::transport::inbound::WebhookInboundTransport;
8use tokio::sync::Mutex;
9
10use crate::handlers::*;
11use crate::metrics::Metrics;
12use crate::state::{AppState, CACHE_CAPACITY};
13
14/// Build the axum router with all routes.
15///
16/// `api_keys` is taken from `BrainConfig.access.api_keys` by the caller.
17/// When `cors_enabled` is true, CORS is restricted to localhost origins (Brain is a local daemon).
18/// When false, no CORS layer is applied (useful for reverse-proxy setups that handle CORS externally).
19pub fn create_router(
20    processor: Arc<signal::SignalProcessor>,
21    webhook_handlers: HashMap<String, Arc<WebhookInboundTransport>>,
22    api_keys: Vec<brain::ApiKeyConfig>,
23    cors_enabled: bool,
24) -> Router {
25    let rate_limits = processor.client_rate_limits().cloned();
26    let state = Arc::new(AppState {
27        processor,
28        webhook_handlers,
29        cache: Mutex::new(lru::LruCache::new(
30            std::num::NonZeroUsize::new(CACHE_CAPACITY).unwrap(),
31        )),
32        api_keys,
33        metrics: Arc::new(Metrics::default()),
34    });
35
36    // Limit concurrent /v1/* requests (100 in-flight per instance)
37    // Limit request body size to 1 MB to prevent memory exhaustion
38    let mut v1_routes = Router::new()
39        .route("/v1/signals", post(post_signal_handler))
40        .route("/v1/signals/:id", get(get_signal_handler))
41        .route("/v1/memory/search", post(search_memory_handler))
42        .route("/v1/memory/facts", get(get_facts_handler))
43        .route("/v1/memory/namespaces", get(get_namespaces_handler))
44        .route("/v1/memory/export", get(export_memory_handler))
45        .route("/v1/memory/import", post(import_memory_handler))
46        .route("/v1/schedules", get(list_schedules_handler))
47        .route("/v1/schedules/:id", delete(cancel_schedule_handler))
48        .route("/v1/events", get(sse_events_handler))
49        .route("/v1/webhooks/:id", post(post_webhook_handler))
50        .layer(axum::extract::DefaultBodyLimit::max(1_048_576))
51        .layer(tower::limit::ConcurrencyLimitLayer::new(100));
52    if let Some(registry) = rate_limits {
53        v1_routes = v1_routes.layer(axum::middleware::from_fn(move |req, next| {
54            let registry = registry.clone();
55            async move { crate::middleware::rate_limit(registry, req, next).await }
56        }));
57    }
58
59    let router = Router::new()
60        .route("/", get(root_handler))
61        .route("/health", get(health_handler))
62        .route("/metrics", get(metrics_handler))
63        .route("/ui", get(ui_handler))
64        .route("/openapi.json", get(openapi_handler))
65        .route("/api", get(swagger_ui_handler))
66        .merge(v1_routes)
67        .with_state(state);
68
69    if cors_enabled {
70        router.layer(brain::cors::localhost_cors())
71    } else {
72        router
73    }
74}
75
76use axum::routing::{get, post};
77
78/// Start the HTTP server, binding to `host:port`.
79///
80/// Blocks until the server shuts down.
81pub async fn serve(
82    processor: Arc<signal::SignalProcessor>,
83    webhook_handlers: HashMap<String, Arc<WebhookInboundTransport>>,
84    host: &str,
85    port: u16,
86) -> anyhow::Result<()> {
87    let cors_enabled = processor.config().adapters.http.cors;
88    let api_keys = processor.config().access.api_keys.clone();
89    let router = create_router(processor, webhook_handlers, api_keys, cors_enabled);
90    let addr: SocketAddr = format!("{host}:{port}").parse()?;
91    tracing::info!("Synapse HTTP online at http://{addr}");
92    let listener = tokio::net::TcpListener::bind(addr).await?;
93    axum::serve(listener, router).await?;
94    Ok(())
95}