1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use uuid::Uuid;
6
7#[derive(Clone, Debug, Serialize, Deserialize, Default)]
9pub struct ProxyConfig {
10 #[serde(default)]
12 pub server: ServerConfig,
13
14 #[serde(default)]
16 pub tls: TlsConfig,
17
18 #[serde(default)]
20 pub rate_limit: RateLimitDefaults,
21
22 #[serde(default)]
24 pub routes: Vec<Route>,
25
26 #[serde(default)]
28 pub upstreams: Vec<Upstream>,
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
33pub struct ServerConfig {
34 #[serde(default = "default_http_host")]
36 pub http_host: String,
37
38 #[serde(default = "default_http_port")]
40 pub http_port: u16,
41
42 #[serde(default)]
50 pub http2_port: Option<u16>,
51
52 #[serde(default = "default_https_host")]
54 pub https_host: String,
55
56 #[serde(default = "default_https_port")]
58 pub https_port: u16,
59
60 #[serde(default)]
62 pub https_enabled: bool,
63
64 #[serde(default = "default_true")]
66 pub database_config: bool,
67
68 pub config_file: Option<String>,
70
71 #[serde(default)]
73 pub watch_config_file: bool,
74}
75
76impl Default for ServerConfig {
77 fn default() -> Self {
78 Self {
79 http_host: default_http_host(),
80 http_port: default_http_port(),
81 http2_port: None,
82 https_host: default_https_host(),
83 https_port: default_https_port(),
84 https_enabled: false,
85 database_config: true,
86 config_file: None,
87 watch_config_file: false,
88 }
89 }
90}
91
92#[derive(Clone, Debug, Serialize, Deserialize)]
94pub struct TlsConfig {
95 #[serde(default)]
97 pub acme_enabled: bool,
98
99 #[serde(default = "default_acme_directory")]
101 pub acme_directory: String,
102
103 pub acme_email: Option<String>,
105
106 #[serde(default = "default_cert_dir")]
108 pub cert_dir: String,
109
110 #[serde(default)]
112 pub acme_staging: bool,
113
114 #[serde(default)]
120 pub cert_file: Option<String>,
121
122 #[serde(default)]
124 pub key_file: Option<String>,
125}
126
127impl Default for TlsConfig {
128 fn default() -> Self {
129 Self {
130 acme_enabled: false,
131 acme_directory: default_acme_directory(),
132 acme_email: None,
133 cert_dir: default_cert_dir(),
134 acme_staging: false,
135 cert_file: None,
136 key_file: None,
137 }
138 }
139}
140
141#[derive(Clone, Debug, Serialize, Deserialize)]
143pub struct RateLimitDefaults {
144 #[serde(default = "default_rate_limit_requests")]
146 pub requests: u32,
147
148 #[serde(default = "default_rate_limit_window")]
150 pub window_secs: u32,
151
152 #[serde(default = "default_burst")]
154 pub burst: u32,
155}
156
157impl Default for RateLimitDefaults {
158 fn default() -> Self {
159 Self {
160 requests: default_rate_limit_requests(),
161 window_secs: default_rate_limit_window(),
162 burst: default_burst(),
163 }
164 }
165}
166
167#[derive(Clone, Debug, Serialize, Deserialize)]
169pub struct Route {
170 pub id: Option<Uuid>,
172
173 pub name: String,
175
176 pub description: Option<String>,
178
179 #[serde(default, rename = "match")]
181 pub match_: RouteMatch,
182
183 #[serde(default = "default_priority")]
185 pub priority: i32,
186
187 pub upstream: String,
189
190 #[serde(default)]
192 pub strip_path: bool,
193
194 #[serde(default)]
196 pub add_headers: HashMap<String, String>,
197
198 #[serde(default)]
200 pub remove_headers: Vec<String>,
201
202 pub rate_limit: Option<RouteRateLimit>,
204
205 #[serde(default = "default_timeout")]
207 pub timeout_secs: u32,
208
209 #[serde(default)]
211 pub retry_count: u32,
212
213 #[serde(default = "default_true")]
215 pub enabled: bool,
216}
217
218#[derive(Clone, Debug, Default, Serialize, Deserialize)]
220pub struct RouteMatch {
221 pub host: Option<String>,
223
224 pub path: Option<String>,
226
227 #[serde(default)]
229 pub path_type: PathMatchType,
230
231 #[serde(default)]
233 pub headers: HashMap<String, String>,
234
235 pub methods: Option<Vec<String>>,
237}
238
239#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
241#[serde(rename_all = "lowercase")]
242pub enum PathMatchType {
243 #[default]
245 Prefix,
246 Exact,
248 Regex,
250}
251
252#[derive(Clone, Debug, Serialize, Deserialize)]
254pub struct RouteRateLimit {
255 pub requests: u32,
257 pub window_secs: u32,
259 #[serde(default)]
261 pub key: RateLimitKey,
262}
263
264#[derive(Clone, Debug, Default, Serialize, Deserialize)]
266#[serde(rename_all = "snake_case")]
267pub enum RateLimitKey {
268 #[default]
270 ClientIp,
271 Header(String),
273 Route,
275}
276
277#[derive(Clone, Debug, Serialize, Deserialize)]
279pub struct Upstream {
280 pub id: Option<Uuid>,
282
283 pub name: String,
285
286 pub description: Option<String>,
288
289 #[serde(default)]
291 pub lb_strategy: LoadBalanceStrategy,
292
293 #[serde(default)]
295 pub backends: Vec<Backend>,
296
297 #[serde(default)]
299 pub health_check: HealthCheckConfig,
300
301 #[serde(default = "default_true")]
303 pub enabled: bool,
304}
305
306impl Upstream {
307 pub fn resolved_id(&self) -> Uuid {
316 self.id
317 .unwrap_or_else(|| Uuid::new_v5(&Uuid::NAMESPACE_OID, self.name.as_bytes()))
318 }
319}
320
321#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
323#[serde(rename_all = "snake_case")]
324pub enum LoadBalanceStrategy {
325 #[default]
327 RoundRobin,
328 LeastConnections,
330 Weighted,
332 Random,
334 Sticky,
336}
337
338#[derive(Clone, Debug, Serialize, Deserialize)]
340pub struct Backend {
341 pub id: Option<Uuid>,
343
344 pub address: String,
346
347 #[serde(default = "default_scheme")]
349 pub scheme: String,
350
351 #[serde(default = "default_weight")]
353 pub weight: u32,
354
355 #[serde(default = "default_true")]
357 pub enabled: bool,
358
359 #[serde(default)]
366 pub http_version: UpstreamHttpVersion,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
371#[serde(rename_all = "lowercase")]
372pub enum UpstreamHttpVersion {
373 #[default]
375 #[serde(alias = "http1", alias = "http/1.1")]
376 Http11,
377 #[serde(alias = "http2", alias = "h2")]
379 H2c,
380}
381
382#[derive(Clone, Debug, Serialize, Deserialize)]
384pub struct HealthCheckConfig {
385 #[serde(default = "default_true")]
387 pub enabled: bool,
388
389 #[serde(default = "default_health_path")]
391 pub path: String,
392
393 #[serde(default = "default_health_interval")]
395 pub interval_secs: u32,
396
397 #[serde(default = "default_health_timeout")]
399 pub timeout_secs: u32,
400
401 #[serde(default = "default_healthy_threshold")]
403 pub healthy_threshold: u32,
404
405 #[serde(default = "default_unhealthy_threshold")]
407 pub unhealthy_threshold: u32,
408}
409
410impl Default for HealthCheckConfig {
411 fn default() -> Self {
412 Self {
413 enabled: true,
414 path: default_health_path(),
415 interval_secs: default_health_interval(),
416 timeout_secs: default_health_timeout(),
417 healthy_threshold: default_healthy_threshold(),
418 unhealthy_threshold: default_unhealthy_threshold(),
419 }
420 }
421}
422
423fn default_http_host() -> String {
425 "0.0.0.0".into()
426}
427fn default_http_port() -> u16 {
428 8080
429}
430fn default_https_host() -> String {
431 "0.0.0.0".into()
432}
433fn default_https_port() -> u16 {
434 8443
435}
436fn default_true() -> bool {
437 true
438}
439fn default_acme_directory() -> String {
440 "https://acme-v02.api.letsencrypt.org/directory".into()
441}
442fn default_cert_dir() -> String {
443 "./certs".into()
444}
445fn default_rate_limit_requests() -> u32 {
446 1000
447}
448fn default_rate_limit_window() -> u32 {
449 60
450}
451fn default_burst() -> u32 {
452 50
453}
454fn default_priority() -> i32 {
455 100
456}
457fn default_timeout() -> u32 {
458 30
459}
460fn default_scheme() -> String {
461 "http".into()
462}
463fn default_weight() -> u32 {
464 100
465}
466fn default_health_path() -> String {
467 "/health".into()
468}
469fn default_health_interval() -> u32 {
470 10
471}
472fn default_health_timeout() -> u32 {
473 5
474}
475fn default_healthy_threshold() -> u32 {
476 2
477}
478fn default_unhealthy_threshold() -> u32 {
479 3
480}
481
482#[derive(Clone, Debug, Serialize, Deserialize, Default)]
484pub struct AcmeConfig {
485 #[serde(default)]
487 pub enabled: bool,
488
489 pub email: Option<String>,
491
492 #[serde(default)]
494 pub staging: bool,
495
496 #[serde(default)]
498 pub domains: Vec<String>,
499}
500
501#[cfg(test)]
502mod upstream_version_tests {
503 use super::*;
504
505 #[test]
506 fn test_backend_defaults_to_http11() {
507 let backend: Backend = toml::from_str(r#"address = "127.0.0.1:8080""#).unwrap();
509 assert_eq!(backend.http_version, UpstreamHttpVersion::Http11);
510 }
511
512 #[test]
513 fn test_backend_h2c_aliases() {
514 for spelling in ["h2c", "h2", "http2"] {
517 let toml_src = format!("address = \"127.0.0.1:8080\"\nhttp_version = \"{spelling}\"");
518 let backend: Backend = toml::from_str(&toml_src).unwrap();
519 assert_eq!(
520 backend.http_version,
521 UpstreamHttpVersion::H2c,
522 "spelling {spelling} should select h2c"
523 );
524 }
525 }
526
527 #[test]
528 fn test_backend_http11_aliases() {
529 for spelling in ["http11", "http1"] {
530 let toml_src = format!("address = \"127.0.0.1:8080\"\nhttp_version = \"{spelling}\"");
531 let backend: Backend = toml::from_str(&toml_src).unwrap();
532 assert_eq!(backend.http_version, UpstreamHttpVersion::Http11);
533 }
534 }
535
536 #[test]
537 fn test_http2_port_is_optional_and_off_by_default() {
538 let server: ServerConfig = toml::from_str("").unwrap();
539 assert_eq!(server.http2_port, None);
540
541 let server: ServerConfig = toml::from_str("http2_port = 19081").unwrap();
542 assert_eq!(server.http2_port, Some(19081));
543 }
544}