v0.0.5
RustBox CI / rustbox (push) Waiting to run

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 12:06:44 +01:00
1 parent 96a32ea8fb
commit a643e7e3ab
106 files changed
+2223 -4669

No files matched your search

+5 -48
View File
@@ -48,14 +48,6 @@ use crate::realtime::events::EventBus;
use crate::realtime::idle::IdleManager;
use crate::smtp::client::SmtpClient;
/// Per-IP key extractor that supports trusted reverse proxies.
///
/// When the direct peer IP is in the configured `trusted_proxies` list,
/// `X-Forwarded-For` is walked from right to left. Trusted proxy hops on the
/// right are skipped and the nearest untrusted address becomes the client IP.
/// If the header is missing or malformed, the direct peer IP is used. Falls
/// back to loopback when `ConnectInfo<SocketAddr>` is unavailable (e.g. in
/// unit tests).
#[derive(Debug, Clone)]
struct ProxyAwareIpExtractor {
trusted_proxies: Vec<TrustedProxy>,
@@ -109,28 +101,6 @@ impl KeyExtractor for ProxyAwareIpExtractor {
}
}
/// Assembles all application routes into an Axum Router.
///
/// Route layout:
/// - `GET /api/health` / `/api/ready` — liveness/readiness (public)
/// - `POST /api/auth/login` — login (public, CSRF only)
/// - `GET /api/auth/session` — get session (auth_guard + CSRF)
/// - `POST /api/auth/logout` — logout (auth_guard + CSRF)
/// - `GET /api/folders` — list folders (auth_guard + CSRF)
/// - `GET /api/folders/:folder/messages` — list messages (auth_guard + CSRF)
/// - `GET /api/messages/:folder/:uid` — get message detail (auth_guard + CSRF)
/// - `PATCH /api/messages/:folder/:uid/flags` — update flags (auth_guard + CSRF)
/// - `GET /api/messages/:folder/:uid/attachments/:attachment_id` — download attachment (auth_guard + CSRF)
/// - `POST /api/messages/move` — move message (auth_guard + CSRF)
/// - `DELETE /api/messages/:folder/:uid` — delete message (auth_guard + CSRF)
///
/// All non-API paths are served by the embedded RustBox UI.
/// The UI assets are compiled into the same executable; there is no frontend runtime.
///
/// Middleware layers:
/// - tower-http tracing
/// - CSRF protection on auth routes
/// - auth_guard on protected routes
pub fn create_router(
config: Arc<AppConfig>,
store: Arc<SessionStore>,
@@ -140,7 +110,6 @@ pub fn create_router(
event_bus: Arc<EventBus>,
idle_manager: Arc<IdleManager>,
) -> Router {
// Rate-limit login: replenish 1 token every 12 s, burst of 5.
let governor_conf = GovernorConfigBuilder::default()
.key_extractor(ProxyAwareIpExtractor {
trusted_proxies: config.parsed_trusted_proxies(),
@@ -150,15 +119,12 @@ pub fn create_router(
.finish()
.expect("valid governor config");
// Public auth route: GovernorLayer (outermost) -> CSRF -> handler.
let public_auth = Router::new()
.route("/login", post(auth::login))
.route("/policy", get(auth::auth_policy))
.layer(middleware::from_fn(csrf_protection))
.layer(GovernorLayer::new(governor_conf));
// Browser-bound routes (no auth_guard, no rate limit).
// These routes only need the browser cookie, not full auth.
let browser_routes = Router::new()
.route("/accounts", get(auth::list_accounts))
.route(
@@ -166,7 +132,6 @@ pub fn create_router(
delete(auth::remove_account).layer(middleware::from_fn(csrf_protection)),
);
// Protected auth routes (auth_guard + CSRF).
let protected_auth = Router::new()
.route("/session", get(auth::get_session))
.route("/sessions", get(auth::list_active_sessions))
@@ -181,9 +146,9 @@ pub fn create_router(
.merge(browser_routes)
.merge(protected_auth);
// Protected data routes (auth_guard + CSRF).
// Multipart framing needs a small allowance on top of the configured per-file limit.
let attachment_body_limit = config.max_attachment_size_bytes().saturating_add(1024 * 1024);
let attachment_body_limit = config
.max_attachment_size_bytes()
.saturating_add(1024 * 1024);
let protected_data = Router::new()
.route(
"/folders",
@@ -240,7 +205,8 @@ pub fn create_router(
)
.route(
"/drafts/{draft_id}/attachments",
post(attachments::upload_attachment).layer(DefaultBodyLimit::max(attachment_body_limit)),
post(attachments::upload_attachment)
.layer(DefaultBodyLimit::max(attachment_body_limit)),
)
.route(
"/drafts/{draft_id}/attachments/{attachment_id}",
@@ -482,8 +448,6 @@ pub fn create_router(
.layer(middleware::from_fn(auth_guard))
.layer(middleware::from_fn(csrf_protection));
// WebSocket route — auth is handled inside the handler via cookie,
// so it bypasses CSRF and auth_guard middleware.
let ws_route = Router::new().route("/ws", get(crate::realtime::ws::ws_handler));
let api_router = Router::new()
@@ -493,21 +457,14 @@ pub fn create_router(
.merge(ws_route)
.merge(protected_data);
// RustBox serves API and UI from the same Rust process.
let inner = Router::new()
.nest("/api", api_router)
// An explicit index route is required for the root of a nested base path
// (for example `/mail/`). The fallback still handles SPA deep links.
.route("/", get(crate::ui::serve_ui))
.fallback(crate::ui::serve_ui);
// Use the same base path for routing and authentication cookies.
let router = match config.normalized_base_path() {
"/" => inner,
base_path => {
// Axum nests an inner `/` route at the prefix itself (`/mail`),
// not at the trailing-slash variant (`/mail/`). Serve that variant
// explicitly so both URLs load the same embedded SPA.
let base_path_slash = format!("{}/", base_path.trim_end_matches('/'));
Router::new()
.route(&base_path_slash, get(crate::ui::serve_ui))