1use axum::Router;
2use axum::body::Body;
3use axum::extract::DefaultBodyLimit;
4use axum::http::{Request, StatusCode};
5use axum::middleware::Next;
6use axum::response::{IntoResponse, Response};
7use axum::routing::{get, post};
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Duration;
11use tower_http::cors::CorsLayer;
12use tower_http::set_header::SetResponseHeaderLayer;
13use tower_http::timeout::TimeoutLayer;
14use tower_http::trace::TraceLayer;
15
16const MAX_BODY_SIZE: usize = 1024 * 1024;
18
19use crate::middleware::auth_middleware;
20use crate::routes::{self, method_not_allowed};
21use crate::state::AppState;
22
23pub fn create_app(state: AppState) -> Router {
24 let api_keys = Arc::new(state.config.auth.api_keys.clone());
25 let timeout = Duration::from_secs(state.config.effective_request_timeout_secs());
30 let rate_limit_rps = state.config.server.rate_limit_rps;
31
32 let firecrawl_compat = Router::new().nest(
48 "/firecrawl",
49 routes::v1::router().merge(routes::v2::router()),
50 );
51 let api_routes = routes::v1::router()
52 .merge(routes::v2::router())
53 .merge(firecrawl_compat)
54 .route(
55 "/mcp",
56 post(routes::mcp::mcp_handler).fallback(method_not_allowed),
57 );
58
59 let api_routes = if api_keys.is_empty() {
60 api_routes.with_state(state.clone())
61 } else {
62 api_routes
63 .route_layer(axum::middleware::from_fn_with_state(
64 api_keys,
65 auth_middleware,
66 ))
67 .with_state(state.clone())
68 };
69
70 let rate_limiter = if rate_limit_rps > 0 {
71 Some(Arc::new(RateLimiter::new(rate_limit_rps)))
72 } else {
73 None
74 };
75
76 Router::new()
77 .route(
78 "/health",
79 get(routes::health::health).fallback(method_not_allowed),
80 )
81 .route(
82 "/openapi.json",
83 get(routes::openapi::serve_openapi_3_1).fallback(method_not_allowed),
84 )
85 .route(
86 "/openapi-3.0.json",
87 get(routes::openapi::serve_openapi_3_0).fallback(method_not_allowed),
88 )
89 .route(
90 "/ready",
91 get(routes::health::ready).fallback(method_not_allowed),
92 )
93 .route(
94 "/metrics",
95 get(routes::metrics::metrics).fallback(method_not_allowed),
96 )
97 .route(
98 "/metrics/renderer-breakers",
99 get(routes::breakers::renderer_breakers).fallback(method_not_allowed),
100 )
101 .route(
102 "/admin/breakers/reset",
103 post(routes::breakers::reset_breakers).fallback(method_not_allowed),
104 )
105 .with_state(state)
106 .merge(api_routes)
107 .layer(axum::middleware::from_fn(move |req, next| {
108 let limiter = rate_limiter.clone();
109 rate_limit_middleware(limiter, req, next)
110 }))
111 .layer(DefaultBodyLimit::max(MAX_BODY_SIZE))
112 .layer(TimeoutLayer::with_status_code(
113 StatusCode::GATEWAY_TIMEOUT,
114 timeout,
115 ))
116 .layer(SetResponseHeaderLayer::overriding(
117 axum::http::header::X_CONTENT_TYPE_OPTIONS,
118 axum::http::HeaderValue::from_static("nosniff"),
119 ))
120 .layer(SetResponseHeaderLayer::overriding(
121 axum::http::header::X_FRAME_OPTIONS,
122 axum::http::HeaderValue::from_static("DENY"),
123 ))
124 .layer(CorsLayer::permissive())
125 .layer(TraceLayer::new_for_http())
126}
127
128struct RateLimiter {
131 tokens: AtomicU64,
132 max_tokens: u64,
133 last_refill: std::sync::Mutex<std::time::Instant>,
134}
135
136impl RateLimiter {
137 fn new(rps: u64) -> Self {
138 Self {
139 tokens: AtomicU64::new(rps),
140 max_tokens: rps,
141 last_refill: std::sync::Mutex::new(std::time::Instant::now()),
142 }
143 }
144
145 fn try_acquire(&self) -> bool {
146 {
148 let mut last = self.last_refill.lock().unwrap();
149 let elapsed = last.elapsed();
150 if elapsed >= Duration::from_secs(1) {
151 let refill = (elapsed.as_secs_f64() * self.max_tokens as f64) as u64;
152 let current = self.tokens.load(Ordering::Relaxed);
153 let new_val = (current + refill).min(self.max_tokens);
154 self.tokens.store(new_val, Ordering::Relaxed);
155 *last = std::time::Instant::now();
156 }
157 }
158
159 loop {
161 let current = self.tokens.load(Ordering::Relaxed);
162 if current == 0 {
163 return false;
164 }
165 if self
166 .tokens
167 .compare_exchange_weak(current, current - 1, Ordering::Relaxed, Ordering::Relaxed)
168 .is_ok()
169 {
170 return true;
171 }
172 }
173 }
174}
175
176async fn rate_limit_middleware(
177 limiter: Option<Arc<RateLimiter>>,
178 req: Request<Body>,
179 next: Next,
180) -> Response {
181 if let Some(limiter) = limiter
182 && req.uri().path() != "/health"
183 && req.uri().path() != "/ready"
184 && !limiter.try_acquire()
185 {
186 return (
187 StatusCode::TOO_MANY_REQUESTS,
188 axum::Json(crw_core::types::ApiResponse::<()>::err_with_code(
189 "Rate limited",
190 "rate_limited",
191 )),
192 )
193 .into_response();
194 }
195 next.run(req).await
196}