mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
feat: router middleware (CORS, body limit, timeout, compression, tracing)
Layer global middleware onto build_router (applies to every route, shared by main + the test harness): - CORS: configurable allowed origins via CORS_ALLOWED_ORIGINS; empty -> allow any origin, which is safe here because auth is a Bearer token (no cookies, no ambient credentials). Credentials are never allowed. - Body limit: 1 MiB global backstop (handlers already validate tighter per field); /wallet/lws/submit_tx overrides to ~2 MiB+ for raw Monero tx hex. - Timeout: 60s request backstop (above the chain clients' ~30s timeouts, so their errors surface first; bounds any slow handler). Uses the http-aware tower_http TimeoutLayer (-> 408), not tower::timeout (fallible, rejected by axum's Infallible Router). - gzip compression + request tracing (outermost). Adds cors_allowed_origins config + the tower-http "timeout" feature. Per-IP rate limiting is deliberately separate (needs ConnectInfo) — next commit. All gates green; integration tests pass through the layered router (CORS/timeout/ compression/body-limit are oneshot-safe). openapi unchanged.
This commit is contained in:
@@ -16,7 +16,12 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::State, http::HeaderMap, routing::post, Json, Router};
|
||||
use axum::{
|
||||
extract::{DefaultBodyLimit, State},
|
||||
http::HeaderMap,
|
||||
routing::post,
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -539,7 +544,12 @@ pub fn routes() -> Router<Arc<AppState>> {
|
||||
.route("/wallet/lws/history", post(history))
|
||||
.route("/wallet/lws/unspent_outs", post(unspent_outs))
|
||||
.route("/wallet/lws/random_outs", post(random_outs))
|
||||
.route("/wallet/lws/submit_tx", post(submit_tx))
|
||||
// submit_tx carries a raw tx hex (up to MAX_CN_TX_HEX_LEN); raise its
|
||||
// body cap above the global limit, with headroom for the JSON envelope.
|
||||
.route(
|
||||
"/wallet/lws/submit_tx",
|
||||
post(submit_tx).layer(DefaultBodyLimit::max(MAX_CN_TX_HEX_LEN + 64 * 1024)),
|
||||
)
|
||||
.route("/wallet/lws/height", post(height))
|
||||
.route("/wallet/lws/confirmations", post(confirmations))
|
||||
}
|
||||
|
||||
@@ -117,6 +117,10 @@ pub struct Config {
|
||||
/// Networks whose `X-Forwarded-For` is trusted. Empty (default) means the
|
||||
/// real TCP peer is always used for rate-limiting and audit IPs.
|
||||
pub trusted_proxies: Vec<IpNetwork>,
|
||||
/// Browser origins allowed by CORS (e.g. the web wallet). Empty (default)
|
||||
/// allows any origin — safe here because auth is a Bearer token, not cookies,
|
||||
/// so no ambient credentials ride a cross-origin request.
|
||||
pub cors_allowed_origins: Vec<String>,
|
||||
|
||||
pub features: FeatureFlags,
|
||||
pub chains: ChainConfig,
|
||||
@@ -296,6 +300,7 @@ impl Config {
|
||||
ip_salt: env_or("IP_SALT", ""),
|
||||
},
|
||||
trusted_proxies: parse_networks("TRUSTED_PROXIES")?,
|
||||
cors_allowed_origins: env_list("CORS_ALLOWED_ORIGINS"),
|
||||
|
||||
features: FeatureFlags {
|
||||
chains: ChainFlags {
|
||||
@@ -533,6 +538,7 @@ mod tests {
|
||||
ip_salt: "s".repeat(16),
|
||||
},
|
||||
trusted_proxies: vec![],
|
||||
cors_allowed_origins: vec![],
|
||||
features: FeatureFlags {
|
||||
chains: ChainFlags {
|
||||
btc: false,
|
||||
|
||||
+46
@@ -17,15 +17,28 @@ pub mod models;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use axum::http::{header, HeaderValue, Method};
|
||||
use axum::{routing::get, Router};
|
||||
use tokio::sync::RwLock;
|
||||
use tower_http::compression::CompressionLayer;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::timeout::TimeoutLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::core::session::{SessionManager, WebChallenge};
|
||||
use crate::infra::chains::ChainClients;
|
||||
use crate::infra::db::Database;
|
||||
|
||||
/// Global request-body cap (backstop; handlers validate tighter per field).
|
||||
const MAX_BODY_BYTES: usize = 1024 * 1024;
|
||||
/// Global request deadline (backstop above the chain clients' own ~30s timeouts,
|
||||
/// so their errors surface first for chain calls; bounds any slow handler).
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Shared application state injected into handlers via `State<Arc<AppState>>`.
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
@@ -44,6 +57,8 @@ pub struct AppState {
|
||||
/// the exact same wiring: `/health` and the NIP-05 directory at the root, the
|
||||
/// authenticated wallet/identity API nested under `/api/v1`.
|
||||
pub fn build_router(state: Arc<AppState>) -> Router {
|
||||
let cors = cors_layer(&state.config);
|
||||
|
||||
let api_v1 = api::auth::routes()
|
||||
.merge(api::website::routes())
|
||||
.merge(api::users::routes())
|
||||
@@ -54,4 +69,35 @@ pub fn build_router(state: Arc<AppState>) -> Router {
|
||||
.merge(api::nip05::routes())
|
||||
.nest("/api/v1", api_v1)
|
||||
.with_state(state)
|
||||
// Layers apply outside-in (last added = outermost). Body cap + timeout
|
||||
// sit closest to the handlers; CORS + compression wrap them; tracing is
|
||||
// outermost so it observes every request (incl. rejected ones).
|
||||
// Per-IP rate limiting is layered separately (it needs ConnectInfo).
|
||||
.layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
|
||||
.layer(TimeoutLayer::new(REQUEST_TIMEOUT))
|
||||
.layer(cors)
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
}
|
||||
|
||||
/// Build the CORS layer. With no configured origins, allow any origin — safe
|
||||
/// because authentication is a Bearer token (no cookies), so a cross-origin
|
||||
/// request carries no ambient credentials. Credentials are never allowed.
|
||||
fn cors_layer(config: &Config) -> CorsLayer {
|
||||
let methods = [Method::GET, Method::POST, Method::OPTIONS];
|
||||
let headers = [header::AUTHORIZATION, header::CONTENT_TYPE];
|
||||
let base = CorsLayer::new()
|
||||
.allow_methods(methods)
|
||||
.allow_headers(headers);
|
||||
|
||||
if config.cors_allowed_origins.is_empty() {
|
||||
base.allow_origin(Any)
|
||||
} else {
|
||||
let origins: Vec<HeaderValue> = config
|
||||
.cors_allowed_origins
|
||||
.iter()
|
||||
.filter_map(|o| o.parse().ok())
|
||||
.collect();
|
||||
base.allow_origin(origins)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user