1pub mod access;
9pub mod acme;
10pub mod alert;
11pub mod auth;
12pub mod budget;
13pub mod config;
14pub mod cors;
15pub mod cp;
16pub mod dlp;
17pub mod doctor;
18pub mod generate;
19pub mod keyvault;
20pub mod limiter;
21pub mod llm;
22pub mod metrics;
23pub mod proxy;
24pub mod reload;
25pub mod scaffold;
26pub mod supervisor;
27pub mod telemetry;
28pub mod tls;
29pub mod waf;
30
31use std::num::NonZeroU32;
32use std::sync::Arc;
33
34use anyhow::{Context, Result};
35use arc_swap::ArcSwap;
36use axum::{
37 extract::DefaultBodyLimit,
38 routing::{any, get, post},
39 Router,
40};
41use governor::{Quota, RateLimiter};
42use hyper_util::client::legacy::Client;
43use hyper_util::rt::TokioExecutor;
44
45use crate::auth::AuthEngine;
46use crate::config::{parse_duration, parse_rate, parse_size, Config};
47use crate::metrics::Metrics;
48use crate::proxy::{
49 csp_report, metrics_handler, ready, AppState, RouteLimiter, Runtime, StrLimiter,
50};
51
52pub use crate::auth::hash_password;
53
54fn quota(rate: &str, burst: u32) -> Result<Quota> {
58 let (count, period) = parse_rate(rate)?;
59 anyhow::ensure!(count > 0, "rate count must be > 0 (got \"{rate}\")");
60 anyhow::ensure!(burst > 0, "burst must be > 0 (rate \"{rate}\")");
61 let per_cell = period / count;
63 let burst = NonZeroU32::new(burst).unwrap();
64 Ok(Quota::with_period(per_cell)
65 .context("rate too high for a usable replenish interval")?
66 .allow_burst(burst))
67}
68
69pub fn build_runtime(cfg: Arc<Config>) -> Result<Runtime> {
75 let rl = &cfg.ratelimit;
76
77 let store_mode = crate::limiter::StoreMode::parse(&rl.store)?;
82 let use_distributed = rl.enabled && store_mode.is_distributed();
83
84 let distributed = if use_distributed {
85 Some(crate::limiter::DistributedLimiter::build(rl, store_mode)?)
86 } else {
87 None
88 };
89
90 let build_local = rl.enabled && !use_distributed;
92
93 let ip_limiter = if build_local {
94 Some(Arc::new(RateLimiter::keyed(quota(&rl.rate, rl.burst)?)))
95 } else {
96 None
97 };
98
99 let mut route_limiters = Vec::new();
100 if build_local {
101 for route in &rl.routes {
102 anyhow::ensure!(
103 !route.path.is_empty(),
104 "ratelimit.routes[].path must not be empty"
105 );
106 route_limiters.push(RouteLimiter {
107 prefix: route.path.clone(),
108 limiter: Arc::new(RateLimiter::keyed(quota(&route.rate, route.burst)?)),
109 });
110 }
111 }
112
113 let key_limiter: Option<Arc<StrLimiter>> = if build_local && rl.per_key.enabled {
114 Some(Arc::new(RateLimiter::keyed(quota(
115 &rl.per_key.rate,
116 rl.per_key.burst,
117 )?)))
118 } else {
119 None
120 };
121
122 let mut upstream_routes = Vec::with_capacity(cfg.upstreams.len());
125 for route in &cfg.upstreams {
126 anyhow::ensure!(!route.path.is_empty(), "upstreams[].path must not be empty");
127 anyhow::ensure!(
131 route.path.starts_with('/'),
132 "upstreams[].path must start with '/' (got {:?})",
133 route.path
134 );
135 anyhow::ensure!(
136 !route.target.is_empty(),
137 "upstreams[].target must not be empty (path {:?})",
138 route.path
139 );
140 let base = route.target.trim_end_matches('/').to_string();
141 upstream_routes.push((route.path.clone(), std::sync::Arc::new(base)));
142 }
143
144 let auth = AuthEngine::build(&cfg.auth)?;
145 let waf = crate::waf::WafEngine::build(&cfg.waf)?;
148 let cors = crate::cors::CorsPolicy::build(&cfg.cors)?;
151 let access = crate::access::AccessPolicy::build(&cfg.access)?;
153
154 let max_body = parse_size(&cfg.validation.max_body)?;
155 let max_response_body = parse_size(&cfg.validation.max_response_body)?;
156 let max_header_bytes = parse_size(&cfg.validation.max_header_bytes)?;
157 let upstream_timeout = parse_duration(&cfg.validation.upstream_timeout)?;
159 let upstream_timeout = (!upstream_timeout.is_zero()).then_some(upstream_timeout);
160
161 Ok(Runtime {
162 upstream_base: Arc::new(cfg.upstream_base()),
163 upstream_routes,
164 auth,
165 waf,
166 cors,
167 access,
168 distributed,
169 ip_limiter,
170 route_limiters,
171 key_limiter,
172 max_body,
173 max_response_body,
174 max_header_bytes,
175 upstream_timeout,
176 stream_passthrough: cfg.validation.stream_passthrough,
177 websocket_passthrough: cfg.validation.websocket_passthrough,
178 llm: {
179 crate::llm::UnpricedPolicy::parse(&cfg.llm.on_unpriced_model)?;
182 Arc::new(crate::llm::LlmRuntime::build(&cfg.llm))
183 },
184 budgets: crate::budget::BudgetEngine::build(&cfg.llm)?.map(Arc::new),
185 keyvault: crate::keyvault::KeyVault::build(&cfg.llm)?.map(Arc::new),
186 dlp: crate::dlp::DlpEngine::build(&cfg.llm.dlp)?.map(Arc::new),
187 telemetry: Arc::new(crate::telemetry::TelemetryRuntime::build(
188 &cfg.llm.telemetry,
189 )),
190 alerts: Arc::new(crate::alert::AlertRuntime::build(&cfg.alerts)),
191 cfg,
192 })
193}
194
195pub fn build_state(cfg: Arc<Config>) -> Result<AppState> {
198 let cp = crate::cp::CpClient::from_cfg(&cfg.control_plane)?;
200 let runtime = build_runtime(cfg)?;
201 let client =
202 Client::builder(TokioExecutor::new()).build_http::<http_body_util::Full<bytes::Bytes>>();
203 Ok(AppState {
204 client,
205 metrics: Arc::new(Metrics::new()),
206 runtime: Arc::new(ArcSwap::from_pointee(runtime)),
207 cp,
208 quota: Arc::new(crate::cp::QuotaState::default()),
209 })
210}
211
212pub fn build_router(state: AppState) -> Router {
219 let router = public_routes()
220 .merge(admin_routes())
221 .layer(DefaultBodyLimit::disable());
222 maybe_compress(router, &state).with_state(state)
223}
224
225fn maybe_compress(router: Router<AppState>, state: &AppState) -> Router<AppState> {
230 use tower_http::compression::predicate::{DefaultPredicate, NotForContentType, Predicate};
231 use tower_http::compression::CompressionLayer;
232
233 if !state.runtime.load().cfg.validation.compress_responses {
234 return router;
235 }
236 let predicate = DefaultPredicate::new().and(NotForContentType::const_new("text/event-stream"));
237 router.layer(CompressionLayer::new().compress_when(predicate))
238}
239
240pub fn build_public_router(state: AppState) -> Router {
244 let router = public_routes().layer(DefaultBodyLimit::disable());
245 maybe_compress(router, &state).with_state(state)
246}
247
248pub fn build_admin_router(state: AppState) -> Router {
253 admin_routes().with_state(state)
254}
255
256fn public_routes() -> Router<AppState> {
259 Router::new()
260 .route(
261 "/__edgeguard/csp-report",
262 post(csp_report).layer(DefaultBodyLimit::max(64 * 1024)),
263 )
264 .fallback(any(proxy::handle))
265}
266
267fn admin_routes() -> Router<AppState> {
269 Router::new()
270 .route("/__edgeguard/health", get(|| async { "ok" }))
271 .route("/__edgeguard/ready", get(ready))
272 .route("/__edgeguard/metrics", get(metrics_handler))
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::config::RateLimitCfg;
279
280 fn cfg_with_ratelimit(rate: &str, burst: u32) -> Config {
281 Config {
282 ratelimit: RateLimitCfg {
283 enabled: true,
284 rate: rate.into(),
285 burst,
286 ..Default::default()
287 },
288 ..Default::default()
289 }
290 }
291
292 #[test]
293 fn build_state_rejects_zero_rate() {
294 assert!(build_state(Arc::new(cfg_with_ratelimit("0/min", 20))).is_err());
297 }
298
299 #[test]
300 fn build_state_rejects_zero_burst() {
301 assert!(build_state(Arc::new(cfg_with_ratelimit("60/min", 0))).is_err());
302 }
303
304 #[test]
305 fn build_runtime_builds_route_and_key_limiters() {
306 let mut cfg = Config::default();
307 cfg.ratelimit.routes = vec![crate::config::RouteRateLimit {
308 path: "/api/".into(),
309 rate: "10/sec".into(),
310 burst: 5,
311 }];
312 cfg.ratelimit.per_key = crate::config::PerKeyRateLimit {
313 enabled: true,
314 rate: "1000/hour".into(),
315 burst: 100,
316 };
317 let rt = build_runtime(Arc::new(cfg)).unwrap();
318 assert_eq!(rt.route_limiters.len(), 1);
319 assert_eq!(rt.route_limiters[0].prefix, "/api/");
320 assert!(rt.key_limiter.is_some());
321 }
322
323 #[test]
324 fn build_runtime_rejects_bad_route_rate() {
325 let mut cfg = Config::default();
326 cfg.ratelimit.routes = vec![crate::config::RouteRateLimit {
327 path: "/api/".into(),
328 rate: "0/sec".into(),
329 burst: 5,
330 }];
331 assert!(build_runtime(Arc::new(cfg)).is_err());
332 }
333
334 #[test]
335 fn build_runtime_validates_upstream_route_paths() {
336 let bad = Config {
338 upstreams: vec![crate::config::UpstreamRoute {
339 path: "api/".into(),
340 target: "http://api:4000".into(),
341 }],
342 ..Default::default()
343 };
344 assert!(build_runtime(Arc::new(bad)).is_err());
345
346 let ok = Config {
348 upstreams: vec![crate::config::UpstreamRoute {
349 path: "/api/".into(),
350 target: "http://api:4000/".into(),
351 }],
352 ..Default::default()
353 };
354 let rt = build_runtime(Arc::new(ok)).unwrap();
355 assert_eq!(rt.pick_upstream("/api/x"), "http://api:4000");
356 assert_eq!(rt.pick_upstream("/other"), rt.upstream_base.as_str());
357 }
358}