1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde_json::Value;
5
6use crate::fetch::FetchKind;
7use crate::predicate::Predicate;
8
9pub type ListenSpec = String;
10
11#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
12pub struct RawRule {
13 pub name: String,
14 pub listen: Vec<ListenSpec>,
15 #[serde(default, rename = "match")]
16 pub match_predicate: Option<Predicate>,
17 #[serde(default)]
18 pub middleware_chain: Vec<MiddlewareRef>,
19 pub terminate: TerminateSpec,
20 #[serde(default)]
31 pub tls: Option<TlsConfig>,
32 #[serde(default = "default_max_body_bytes")]
35 pub max_body_bytes_request: usize,
36 #[serde(default = "default_max_body_bytes")]
39 pub max_body_bytes_response: usize,
40 #[serde(default)]
41 pub source: SourceInfo,
42}
43
44fn default_max_body_bytes() -> usize {
45 8 * 1024 * 1024
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
61pub struct TlsConfig {
62 #[serde(default)]
63 pub sni: Option<String>,
64 pub cert_file: PathBuf,
65 pub key_file: PathBuf,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
77pub struct ListenerTlsSpec {
78 #[serde(default)]
79 pub default: Option<TlsConfig>,
80 #[serde(default)]
81 pub sni_certs: BTreeMap<String, TlsConfig>,
82}
83
84impl ListenerTlsSpec {
85 #[must_use]
86 pub fn is_empty(&self) -> bool {
87 self.default.is_none() && self.sni_certs.is_empty()
88 }
89}
90
91#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
92pub struct MiddlewareRef {
93 #[serde(rename = "use")]
94 pub name: String,
95 #[serde(default)]
96 pub args: Value,
97 #[serde(default)]
98 pub on_error: Option<OnErrorSpec>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
102pub enum OnErrorSpec {
103 Close,
104 Response(SynthResponse),
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
108pub struct SynthResponse {
109 pub status: u16,
110 #[serde(default)]
111 pub headers: Option<BTreeMap<String, String>>,
112 #[serde(default)]
113 pub body: Option<String>,
114}
115
116impl<'de> serde::Deserialize<'de> for OnErrorSpec {
117 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
118 #[derive(serde::Deserialize)]
119 #[serde(untagged)]
120 enum Raw {
121 Literal(String),
122 Response { response: SynthResponse },
123 }
124 match Raw::deserialize(de)? {
125 Raw::Literal(s) if s == "close" => Ok(Self::Close),
126 Raw::Literal(other) => Err(serde::de::Error::unknown_variant(&other, &["close"])),
127 Raw::Response { response } => Ok(Self::Response(response)),
128 }
129 }
130}
131
132#[derive(Debug, Clone, serde::Serialize)]
133pub struct TerminateSpec {
134 pub kind: FetchKind,
135 pub args: Value,
136}
137
138impl<'de> serde::Deserialize<'de> for TerminateSpec {
139 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
140 let mut v = Value::deserialize(de)?;
141 let obj = v
142 .as_object_mut()
143 .ok_or_else(|| serde::de::Error::custom("`terminate` must be a JSON object"))?;
144 let type_val = obj.remove("type").ok_or_else(|| serde::de::Error::missing_field("type"))?;
145 let Value::String(alias) = type_val else {
146 return Err(serde::de::Error::custom("`terminate.type` must be a string"));
147 };
148 let kind = fetch_kind_from_alias(&alias)
149 .ok_or_else(|| serde::de::Error::custom(format!("unknown terminate type: {alias:?}")))?;
150 Ok(Self { kind, args: v })
151 }
152}
153
154fn fetch_kind_from_alias(alias: &str) -> Option<FetchKind> {
155 match alias {
156 "tcp_forward" | "udp_forward" => Some(FetchKind::L4Forward),
157 "http_proxy" | "http1_proxy" | "http2_proxy" | "http3_proxy" | "unix_proxy" | "cgi" => {
158 Some(FetchKind::HttpProxy)
159 }
160 "websocket" => Some(FetchKind::WebSocketUpgrade),
161 "static" | "redirect_https" => Some(FetchKind::HttpSynthesize),
162 _ => None,
163 }
164}
165
166#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
167pub struct SourceInfo {
168 #[serde(default)]
169 pub file: PathBuf,
170 #[serde(default)]
171 pub line: u32,
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::predicate::{CheckMap, FieldPath, Operator, Predicate, Value as PredValue};
178
179 #[test]
180 fn raw_rule_minimal_parses_with_defaults() {
181 let raw = serde_json::json!({
182 "name": "r",
183 "listen": [":443"],
184 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
185 });
186 let rule: RawRule = serde_json::from_value(raw).expect("parse minimal rule");
187 assert_eq!(rule.name, "r");
188 assert_eq!(rule.listen, vec![":443".to_string()]);
189 assert!(rule.match_predicate.is_none());
190 assert!(rule.middleware_chain.is_empty());
191 assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
192 assert_eq!(rule.terminate.args, serde_json::json!({ "upstream": "127.0.0.1:8080" }));
193 assert_eq!(rule.source.file, PathBuf::new());
194 assert_eq!(rule.source.line, 0);
195 assert_eq!(rule.max_body_bytes_request, 8 * 1024 * 1024);
196 assert_eq!(rule.max_body_bytes_response, 8 * 1024 * 1024);
197 }
198
199 #[test]
200 fn raw_rule_full_populates_every_field() {
201 let raw = serde_json::json!({
202 "name": "api",
203 "listen": [":443", "0.0.0.0:80"],
204 "match": { "tls.sni": { "equals": "api.example.com" } },
205 "middleware_chain": [
206 { "use": "rate_limit", "args": { "rate": 100 } },
207 { "use": "jwt", "args": { "secret": "x" }, "on_error": "close" },
208 ],
209 "terminate": {
210 "type": "http_proxy",
211 "upstream": "127.0.0.1:8080",
212 "timeouts": { "connect": "5s" }
213 },
214 "source": { "file": "rules/30-api.json", "line": 14 },
215 });
216 let rule: RawRule = serde_json::from_value(raw).expect("parse full rule");
217 assert_eq!(rule.name, "api");
218 assert_eq!(rule.listen.len(), 2);
219 let check = match rule.match_predicate.as_ref().expect("match present") {
220 Predicate::Check(c) => c,
221 other => panic!("expected Check, got {other:?}"),
222 };
223 assert_eq!(check.path, FieldPath::TlsSni);
224 match &check.op {
225 Operator::Equals(PredValue::Str(s)) => assert_eq!(s, "api.example.com"),
226 other => panic!("unexpected op: {other:?}"),
227 }
228 assert_eq!(rule.middleware_chain.len(), 2);
229 assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
230 assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
231 assert_eq!(
232 rule.terminate.args,
233 serde_json::json!({
234 "upstream": "127.0.0.1:8080",
235 "timeouts": { "connect": "5s" }
236 }),
237 );
238 assert_eq!(rule.source.file, PathBuf::from("rules/30-api.json"));
239 assert_eq!(rule.source.line, 14);
240 }
241
242 #[test]
243 fn middleware_ref_flat_form_parses_name_and_args() {
244 let raw = serde_json::json!({ "use": "rate_limit", "args": { "rate": 100 } });
245 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
246 assert_eq!(m.name, "rate_limit");
247 assert_eq!(m.args, serde_json::json!({ "rate": 100 }));
248 assert!(m.on_error.is_none());
249 }
250
251 #[test]
252 fn middleware_ref_on_error_close_form() {
253 let raw = serde_json::json!({ "use": "jwt", "args": { "secret": "x" }, "on_error": "close" });
254 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
255 assert_eq!(m.on_error, Some(OnErrorSpec::Close));
256 }
257
258 #[test]
259 fn middleware_ref_on_error_response_object_form() {
260 let raw = serde_json::json!({
261 "use": "jwt",
262 "on_error": { "response": { "status": 503, "body": "maintenance" } },
263 });
264 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
265 assert_eq!(m.name, "jwt");
266 assert_eq!(m.args, Value::Null);
267 let resp = match m.on_error.expect("on_error present") {
268 OnErrorSpec::Response(r) => r,
269 OnErrorSpec::Close => panic!("expected Response"),
270 };
271 assert_eq!(resp.status, 503);
272 assert_eq!(resp.body.as_deref(), Some("maintenance"));
273 assert!(resp.headers.is_none());
274 }
275
276 #[test]
277 fn middleware_ref_args_defaults_to_null_when_omitted() {
278 let raw = serde_json::json!({ "use": "tag" });
279 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
280 assert_eq!(m.args, Value::Null);
281 }
282
283 #[test]
284 fn middleware_ref_requires_use_key() {
285 let raw = serde_json::json!({});
286 let err = serde_json::from_value::<MiddlewareRef>(raw).expect_err("missing `use` must fail");
287 let _ = err.to_string();
288 }
289
290 #[test]
291 fn on_error_spec_string_invalid_variant_rejected() {
292 let raw = serde_json::json!("crash");
293 let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("non-`close` literal rejected");
294 let msg = err.to_string();
295 assert!(msg.contains("close"), "error names the only valid literal: {msg}");
296 }
297
298 #[test]
299 fn on_error_spec_malformed_response_object_rejected() {
300 let raw = serde_json::json!({ "response": null });
301 let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("null response rejected");
302 let _ = err.to_string();
303 }
304
305 #[test]
306 fn on_error_spec_close_literal_parses() {
307 let raw = serde_json::json!("close");
308 let s: OnErrorSpec = serde_json::from_value(raw).expect("close literal parses");
309 assert_eq!(s, OnErrorSpec::Close);
310 }
311
312 #[test]
313 fn on_error_spec_response_object_parses() {
314 let raw = serde_json::json!({
315 "response": { "status": 503, "body": "maintenance" },
316 });
317 let s: OnErrorSpec = serde_json::from_value(raw).expect("response object parses");
318 match s {
319 OnErrorSpec::Response(r) => {
320 assert_eq!(r.status, 503);
321 assert_eq!(r.body.as_deref(), Some("maintenance"));
322 assert!(r.headers.is_none());
323 }
324 OnErrorSpec::Close => panic!("expected Response"),
325 }
326 }
327
328 #[test]
329 fn synth_response_minimal_status_only() {
330 let raw = serde_json::json!({ "status": 200 });
331 let r: SynthResponse = serde_json::from_value(raw).expect("parse status-only synth");
332 assert_eq!(r.status, 200);
333 assert!(r.headers.is_none());
334 assert!(r.body.is_none());
335 }
336
337 #[test]
338 fn synth_response_full_status_headers_body() {
339 let raw = serde_json::json!({
340 "status": 404,
341 "headers": { "content-type": "text/plain" },
342 "body": "not found",
343 });
344 let r: SynthResponse = serde_json::from_value(raw).expect("parse full synth");
345 assert_eq!(r.status, 404);
346 let headers = r.headers.as_ref().expect("headers present");
347 assert_eq!(headers.get("content-type").map(String::as_str), Some("text/plain"));
348 assert_eq!(r.body.as_deref(), Some("not found"));
349 }
350
351 #[test]
352 fn terminate_spec_alias_table_maps_to_fetch_kind() {
353 let cases: &[(&str, FetchKind)] = &[
355 ("tcp_forward", FetchKind::L4Forward),
356 ("udp_forward", FetchKind::L4Forward),
357 ("http_proxy", FetchKind::HttpProxy),
358 ("http1_proxy", FetchKind::HttpProxy),
359 ("http2_proxy", FetchKind::HttpProxy),
360 ("http3_proxy", FetchKind::HttpProxy),
361 ("unix_proxy", FetchKind::HttpProxy),
362 ("cgi", FetchKind::HttpProxy),
363 ("websocket", FetchKind::WebSocketUpgrade),
364 ("static", FetchKind::HttpSynthesize),
365 ("redirect_https", FetchKind::HttpSynthesize),
366 ];
367 for (alias, expected) in cases {
368 let raw = serde_json::json!({ "type": alias });
369 let t: TerminateSpec =
370 serde_json::from_value(raw).unwrap_or_else(|e| panic!("alias {alias} must parse: {e}"));
371 assert_eq!(t.kind, *expected, "alias {alias} must map to {expected:?}");
372 }
373 }
374
375 #[test]
376 fn terminate_spec_args_preserves_all_non_type_keys_verbatim() {
377 let raw = serde_json::json!({
380 "type": "http_proxy",
381 "upstream": "127.0.0.1:8080",
382 "timeouts": { "connect": "5s", "total": "60s" },
383 });
384 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
385 assert_eq!(t.kind, FetchKind::HttpProxy);
386 assert_eq!(
387 t.args,
388 serde_json::json!({
389 "upstream": "127.0.0.1:8080",
390 "timeouts": { "connect": "5s", "total": "60s" },
391 }),
392 );
393 }
394
395 #[test]
396 fn terminate_spec_alias_only_yields_empty_object_not_null() {
397 let raw = serde_json::json!({ "type": "http_proxy" });
401 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
402 assert_eq!(t.kind, FetchKind::HttpProxy);
403 assert_eq!(t.args, serde_json::Value::Object(serde_json::Map::new()));
404 assert!(t.args.is_object(), "args must be an object, got {:?}", t.args);
405 }
406
407 #[test]
408 fn terminate_spec_unknown_type_rejected_and_names_alias() {
409 let raw = serde_json::json!({ "type": "bogus" });
410 let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("unknown alias rejected");
411 assert!(err.to_string().contains("bogus"), "error must name the offending alias: {err}");
412 }
413
414 #[test]
415 fn terminate_spec_missing_type_rejected_and_names_field() {
416 let raw = serde_json::json!({ "upstream": "127.0.0.1:8080" });
417 let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("missing type rejected");
418 assert!(err.to_string().contains("type"), "error must name the missing field: {err}");
419 }
420
421 #[test]
422 fn source_info_default_is_empty_path_and_zero_line() {
423 let s = SourceInfo::default();
424 assert_eq!(s.file, PathBuf::new());
425 assert_eq!(s.line, 0);
426 }
427
428 #[test]
429 fn source_info_round_trip_via_json() {
430 let raw = serde_json::json!({ "file": "rules/a.json", "line": 7 });
431 let s: SourceInfo = serde_json::from_value(raw).expect("parse source info");
432 assert_eq!(s.file, PathBuf::from("rules/a.json"));
433 assert_eq!(s.line, 7);
434 }
435
436 #[test]
437 fn middleware_chain_defaults_to_empty_when_omitted() {
438 let raw = serde_json::json!({
439 "name": "r",
440 "listen": [":443"],
441 "terminate": { "type": "http_proxy" },
442 });
443 let rule: RawRule = serde_json::from_value(raw).expect("parse");
444 assert!(rule.middleware_chain.is_empty());
445 }
446
447 #[test]
448 fn middleware_ref_chain_mixes_on_error_forms() {
449 let raw = serde_json::json!({
450 "name": "r",
451 "listen": [":443"],
452 "middleware_chain": [
453 { "use": "a" },
454 { "use": "b", "on_error": "close" },
455 { "use": "c", "on_error": { "response": { "status": 500 } } },
456 ],
457 "terminate": { "type": "http_proxy" },
458 });
459 let rule: RawRule = serde_json::from_value(raw).expect("parse");
460 assert_eq!(rule.middleware_chain.len(), 3);
461 assert!(rule.middleware_chain[0].on_error.is_none());
462 assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
463 match rule.middleware_chain[2].on_error.as_ref().expect("on_error[2]") {
464 OnErrorSpec::Response(r) => {
465 assert_eq!(r.status, 500);
466 assert!(r.body.is_none());
467 assert!(r.headers.is_none());
468 }
469 OnErrorSpec::Close => panic!("expected Response at index 2"),
470 }
471 }
472
473 #[test]
474 fn raw_rule_accepts_top_level_check_predicate() {
475 let raw = serde_json::json!({
476 "name": "r",
477 "listen": [":80"],
478 "match": { "http.uri.path": { "prefix": "/api" } },
479 "terminate": { "type": "http_proxy" },
480 });
481 let rule: RawRule = serde_json::from_value(raw).expect("parse");
482 let Some(Predicate::Check(CheckMap { path, op })) = rule.match_predicate else {
483 panic!("expected Check predicate");
484 };
485 assert_eq!(path, FieldPath::HttpUriPath);
486 match op {
487 Operator::Prefix(PredValue::Str(s)) => assert_eq!(s, "/api"),
488 other => panic!("unexpected op: {other:?}"),
489 }
490 }
491
492 #[test]
493 fn raw_rule_without_tls_field_defaults_to_none() {
494 let raw = serde_json::json!({
495 "name": "r",
496 "listen": [":80"],
497 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
498 });
499 let rule: RawRule = serde_json::from_value(raw).expect("parse rule without tls");
500 assert!(rule.tls.is_none());
501 }
502
503 #[test]
504 fn raw_rule_with_tls_field_parses_paths() {
505 let raw = serde_json::json!({
506 "name": "r",
507 "listen": [":443"],
508 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
509 "tls": { "cert_file": "/etc/vaned/certs/api.pem", "key_file": "/etc/vaned/certs/api.key" },
510 });
511 let rule: RawRule = serde_json::from_value(raw).expect("parse rule with tls");
512 let tls = rule.tls.expect("tls present");
513 assert_eq!(tls.cert_file, PathBuf::from("/etc/vaned/certs/api.pem"));
514 assert_eq!(tls.key_file, PathBuf::from("/etc/vaned/certs/api.key"));
515 }
516
517 #[test]
518 fn tls_config_round_trips_through_json() {
519 let original = TlsConfig {
520 sni: None,
521 cert_file: PathBuf::from("/srv/cert.pem"),
522 key_file: PathBuf::from("/srv/key.pem"),
523 };
524 let encoded = serde_json::to_string(&original).expect("serialize");
525 let decoded: TlsConfig = serde_json::from_str(&encoded).expect("deserialize");
526 assert_eq!(decoded, original);
527 }
528
529 #[test]
530 fn tls_config_with_sni_field_parses() {
531 let raw = serde_json::json!({
532 "sni": "api.example.com",
533 "cert_file": "/etc/vaned/certs/api.pem",
534 "key_file": "/etc/vaned/certs/api.key",
535 });
536 let tls: TlsConfig = serde_json::from_value(raw).expect("parse tls with sni");
537 assert_eq!(tls.sni.as_deref(), Some("api.example.com"));
538 }
539
540 #[test]
541 fn tls_config_without_sni_parses_with_none() {
542 let raw = serde_json::json!({
545 "cert_file": "/etc/vaned/certs/default.pem",
546 "key_file": "/etc/vaned/certs/default.key",
547 });
548 let tls: TlsConfig = serde_json::from_value(raw).expect("parse tls without sni");
549 assert!(tls.sni.is_none());
550 }
551}