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