1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3use std::time::Duration;
4
5use serde_json::Value;
6
7use crate::error::Error;
8use crate::fetch::FetchKind;
9use crate::predicate::Predicate;
10
11pub type ListenSpec = String;
12
13#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
14pub struct RawRule {
15 pub name: String,
16 pub listen: Vec<ListenSpec>,
17 #[serde(default, rename = "match")]
18 pub match_predicate: Option<Predicate>,
19 #[serde(default)]
20 pub middleware_chain: Vec<MiddlewareRef>,
21 pub terminate: TerminateSpec,
22 #[serde(default)]
33 pub tls: Option<TlsConfig>,
34 #[serde(default)]
40 pub allow_zero_rtt: Option<bool>,
41 #[serde(default = "default_max_body_bytes")]
44 pub max_body_bytes_request: usize,
45 #[serde(default = "default_max_body_bytes")]
48 pub max_body_bytes_response: usize,
49 #[serde(default)]
50 pub source: SourceInfo,
51}
52
53fn default_max_body_bytes() -> usize {
54 8 * 1024 * 1024
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
70pub struct TlsConfig {
71 #[serde(default)]
72 pub sni: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub cert_file: Option<PathBuf>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub key_file: Option<PathBuf>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub managed: Option<ManagedSpec>,
88 pub enable_zero_rtt: bool,
93 #[serde(default)]
98 pub client_auth: Option<ClientAuthConfig>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub ocsp_path: Option<PathBuf>,
109 #[serde(default, skip_serializing_if = "is_default_false")]
115 pub ocsp_fetch: bool,
116}
117
118#[allow(
119 clippy::trivially_copy_pass_by_ref,
120 reason = "serde skip_serializing_if requires fn(&T) -> bool"
121)]
122fn is_default_false(b: &bool) -> bool {
123 !*b
124}
125
126impl TlsConfig {
127 #[must_use]
130 pub const fn is_managed(&self) -> bool {
131 self.managed.is_some()
132 }
133
134 #[must_use]
140 pub const fn is_static(&self) -> bool {
141 self.managed.is_none() && self.cert_file.is_some() && self.key_file.is_some()
142 }
143
144 #[must_use]
149 pub fn static_paths(&self) -> Option<(&Path, &Path)> {
150 match (&self.cert_file, &self.key_file, &self.managed) {
151 (Some(c), Some(k), None) => Some((c.as_path(), k.as_path())),
152 _ => None,
153 }
154 }
155
156 pub fn validate(&self) -> Result<(), Error> {
171 if self.ocsp_path.is_some() && self.ocsp_fetch {
178 return Err(Error::compile(
179 "tls: `ocsp_path` and `ocsp_fetch` are mutually exclusive — pick one OCSP source",
180 ));
181 }
182 let static_present = self.cert_file.is_some() || self.key_file.is_some();
183 match (static_present, &self.managed) {
184 (true, Some(_)) => Err(Error::compile(
185 "tls: `managed` must not coexist with `cert_file` / `key_file` — pick one source",
186 )),
187 (false, None) => Err(Error::compile(
188 "tls: missing cert source — set either `cert_file` + `key_file` or `managed`",
189 )),
190 (true, None) => match (&self.cert_file, &self.key_file) {
191 (Some(_), Some(_)) => Ok(()),
192 (Some(_), None) => {
193 Err(Error::compile("tls: `key_file` is required when `cert_file` is set"))
194 }
195 (None, Some(_)) => {
196 Err(Error::compile("tls: `cert_file` is required when `key_file` is set"))
197 }
198 (None, None) => unreachable!("static_present implies one path is Some"),
199 },
200 (false, Some(m)) => m.validate(self.sni.as_deref()),
201 }
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
213pub struct ManagedSpec {
214 pub directory_url: String,
215 pub contact: Vec<String>,
216 pub agree_tos: bool,
217 pub challenge: ChallengeKind,
218 pub key_type: ManagedKeyType,
219 pub renew_before: String,
224 pub san: Vec<String>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub account_key_path: Option<PathBuf>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub dns_provider: Option<Value>,
234}
235
236impl ManagedSpec {
237 pub fn renew_before_duration(&self) -> Result<Duration, Error> {
244 parse_renewal_duration(&self.renew_before)
245 }
246
247 fn validate(&self, tls_sni: Option<&str>) -> Result<(), Error> {
254 if !self.agree_tos {
255 return Err(Error::compile("tls.managed.agree_tos must be true"));
256 }
257 if self.contact.is_empty() {
258 return Err(Error::compile("tls.managed.contact must list at least one URI"));
259 }
260 if self.directory_url.trim().is_empty() {
261 return Err(Error::compile("tls.managed.directory_url must not be empty"));
262 }
263 if self.san.is_empty() {
264 return Err(Error::compile("tls.managed.san must list at least one name"));
265 }
266 match tls_sni {
267 Some(sni) if !self.san.iter().any(|s| s.eq_ignore_ascii_case(sni)) => {
268 return Err(Error::compile(format!("tls.managed.san must contain tls.sni ({sni:?})")));
269 }
270 None => {
271 return Err(Error::compile("tls.managed requires tls.sni — managed certs are SNI-keyed"));
272 }
273 Some(_) => {}
274 }
275 match (self.challenge, self.dns_provider.is_some()) {
276 (ChallengeKind::Dns01, false) => {
277 return Err(Error::compile("tls.managed: challenge \"dns-01\" requires `dns_provider`"));
278 }
279 (ChallengeKind::Http01, true) => {
280 return Err(Error::compile(
281 "tls.managed: `dns_provider` is only meaningful when challenge == \"dns-01\"",
282 ));
283 }
284 _ => {}
285 }
286 if matches!(self.challenge, ChallengeKind::Http01) {
287 for san in &self.san {
288 if san.starts_with("*.") {
289 return Err(Error::compile(format!(
290 "tls.managed: wildcard SAN {san:?} requires challenge \"dns-01\""
291 )));
292 }
293 }
294 }
295 let renew = self.renew_before_duration()?;
296 if renew.is_zero() {
297 return Err(Error::compile("tls.managed.renew_before must be > 0"));
298 }
299 Ok(())
300 }
301}
302
303#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
304pub enum ChallengeKind {
305 #[serde(rename = "http-01")]
306 Http01,
307 #[serde(rename = "dns-01")]
308 Dns01,
309}
310
311#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
312pub enum ManagedKeyType {
313 #[serde(rename = "ecdsa-p256")]
314 EcdsaP256,
315 #[serde(rename = "rsa-2048")]
316 Rsa2048,
317}
318
319fn parse_renewal_duration(s: &str) -> Result<Duration, Error> {
330 let s = s.trim();
331 if s.is_empty() {
332 return Err(Error::compile("duration must be non-empty"));
333 }
334 let (num, unit_secs) = if let Some(rest) = s.strip_suffix("ms") {
335 (rest, None) } else if let Some(rest) = s.strip_suffix('s') {
337 (rest, Some(1u64))
338 } else if let Some(rest) = s.strip_suffix('m') {
339 (rest, Some(60u64))
340 } else if let Some(rest) = s.strip_suffix('h') {
341 (rest, Some(60 * 60))
342 } else if let Some(rest) = s.strip_suffix('d') {
343 (rest, Some(60 * 60 * 24))
344 } else {
345 return Err(Error::compile(format!(
346 "duration {s:?}: missing unit (expected ms / s / m / h / d)"
347 )));
348 };
349 let n: u64 = num.trim().parse().map_err(|e| Error::compile(format!("duration {s:?}: {e}")))?;
350 Ok(match unit_secs {
351 None => Duration::from_millis(n),
352 Some(secs) => Duration::from_secs(n.saturating_mul(secs)),
353 })
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
361pub struct ClientAuthConfig {
362 pub mode: ClientAuthMode,
363 #[serde(default)]
364 pub trust_store: Option<ClientTrustStoreConfig>,
365}
366
367#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
369#[serde(rename_all = "lowercase")]
370pub enum ClientAuthMode {
371 None,
372 Request,
373 Require,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
379pub struct ClientTrustStoreConfig {
380 #[serde(default)]
381 pub ca_paths: Vec<PathBuf>,
382 #[serde(default)]
383 pub ca_dir: Option<PathBuf>,
384 #[serde(default)]
385 pub crls: Vec<CrlSourceConfig>,
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
393#[serde(tag = "kind", rename_all = "lowercase")]
394pub enum CrlSourceConfig {
395 File { path: PathBuf, fetch_failure: CrlFetchFailure },
396 Url { url: String, fetch_failure: CrlFetchFailure },
397}
398
399#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
401#[serde(rename_all = "lowercase")]
402pub enum CrlFetchFailure {
403 Tolerate,
404 Reject,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
416pub struct ListenerTlsSpec {
417 #[serde(default)]
418 pub default: Option<TlsConfig>,
419 #[serde(default)]
420 pub sni_certs: BTreeMap<String, TlsConfig>,
421 #[serde(default)]
431 pub managed_snis: BTreeMap<String, ManagedSpec>,
432 #[serde(default)]
437 pub client_auth: ClientAuthSpec,
438 #[serde(default)]
446 pub enable_zero_rtt: bool,
447}
448
449impl ListenerTlsSpec {
450 #[must_use]
451 pub fn is_empty(&self) -> bool {
452 self.default.is_none()
453 && self.sni_certs.is_empty()
454 && self.managed_snis.is_empty()
455 && matches!(self.client_auth, ClientAuthSpec::None)
456 && !self.enable_zero_rtt
457 }
458}
459
460#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]
464#[serde(tag = "mode", rename_all = "lowercase")]
465pub enum ClientAuthSpec {
466 #[default]
467 None,
468 Request {
469 trust_store: ClientTrustStoreConfig,
470 },
471 Require {
472 trust_store: ClientTrustStoreConfig,
473 },
474}
475
476#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
477pub struct MiddlewareRef {
478 #[serde(rename = "use")]
479 pub name: String,
480 #[serde(default)]
481 pub args: Value,
482 #[serde(default)]
483 pub on_error: Option<OnErrorSpec>,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
487pub enum OnErrorSpec {
488 Close,
489 Response(SynthResponse),
490}
491
492#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
493pub struct SynthResponse {
494 pub status: u16,
495 #[serde(default)]
496 pub headers: Option<BTreeMap<String, String>>,
497 #[serde(default)]
498 pub body: Option<String>,
499}
500
501impl<'de> serde::Deserialize<'de> for OnErrorSpec {
502 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
503 #[derive(serde::Deserialize)]
504 #[serde(untagged)]
505 enum Raw {
506 Literal(String),
507 Response { response: SynthResponse },
508 }
509 match Raw::deserialize(de)? {
510 Raw::Literal(s) if s == "close" => Ok(Self::Close),
511 Raw::Literal(other) => Err(serde::de::Error::unknown_variant(&other, &["close"])),
512 Raw::Response { response } => Ok(Self::Response(response)),
513 }
514 }
515}
516
517#[derive(Debug, Clone, serde::Serialize)]
518pub struct TerminateSpec {
519 pub kind: FetchKind,
520 pub args: Value,
521}
522
523impl<'de> serde::Deserialize<'de> for TerminateSpec {
524 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
525 let mut v = Value::deserialize(de)?;
526 let obj = v
527 .as_object_mut()
528 .ok_or_else(|| serde::de::Error::custom("`terminate` must be a JSON object"))?;
529 let type_val = obj.remove("type").ok_or_else(|| serde::de::Error::missing_field("type"))?;
530 let Value::String(alias) = type_val else {
531 return Err(serde::de::Error::custom("`terminate.type` must be a string"));
532 };
533 let kind = fetch_kind_from_alias(&alias)
534 .ok_or_else(|| serde::de::Error::custom(format!("unknown terminate type: {alias:?}")))?;
535 if let Some(version) = http_version_from_alias(&alias)
541 && !obj.contains_key("version")
542 {
543 obj.insert("version".to_owned(), Value::String(version.to_owned()));
544 }
545 if let Some(transport) = transport_from_alias(&alias)
550 && !obj.contains_key("transport")
551 {
552 obj.insert("transport".to_owned(), Value::String(transport.to_owned()));
553 }
554 if let Some(upstream_kind) = upstream_kind_from_alias(&alias)
566 && !obj.contains_key("upstream_kind")
567 {
568 obj.insert("upstream_kind".to_owned(), Value::String(upstream_kind.to_owned()));
569 }
570 Ok(Self { kind, args: v })
571 }
572}
573
574fn fetch_kind_from_alias(alias: &str) -> Option<FetchKind> {
575 match alias {
576 "tcp_forward" | "udp_forward" => Some(FetchKind::L4Forward),
577 "http_proxy" | "http1_proxy" | "http2_proxy" | "http3_proxy" | "unix_proxy" | "cgi" => {
578 Some(FetchKind::HttpProxy)
579 }
580 "websocket" => Some(FetchKind::WebSocketUpgrade),
581 "static" | "redirect_https" => Some(FetchKind::HttpSynthesize),
582 _ => None,
583 }
584}
585
586fn http_version_from_alias(alias: &str) -> Option<&'static str> {
587 match alias {
588 "http1_proxy" => Some("h1"),
589 "http2_proxy" => Some("h2"),
590 "http3_proxy" => Some("h3"),
591 _ => None,
592 }
593}
594
595fn transport_from_alias(alias: &str) -> Option<&'static str> {
596 match alias {
597 "tcp_forward" => Some("tcp"),
598 "udp_forward" => Some("udp"),
599 _ => None,
600 }
601}
602
603fn upstream_kind_from_alias(alias: &str) -> Option<&'static str> {
604 match alias {
605 "http_proxy" | "http1_proxy" | "http2_proxy" | "http3_proxy" | "unix_proxy" => Some("tcp"),
606 "cgi" => Some("cgi"),
607 _ => None,
608 }
609}
610
611#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
612pub struct SourceInfo {
613 #[serde(default)]
614 pub file: PathBuf,
615 #[serde(default)]
616 pub line: u32,
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622 use crate::predicate::{CheckMap, FieldPath, Operator, Predicate, Value as PredValue};
623
624 #[test]
625 fn raw_rule_minimal_parses_with_defaults() {
626 let raw = serde_json::json!({
627 "name": "r",
628 "listen": [":443"],
629 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
630 });
631 let rule: RawRule = serde_json::from_value(raw).expect("parse minimal rule");
632 assert_eq!(rule.name, "r");
633 assert_eq!(rule.listen, vec![":443".to_string()]);
634 assert!(rule.match_predicate.is_none());
635 assert!(rule.middleware_chain.is_empty());
636 assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
637 assert_eq!(
638 rule.terminate.args,
639 serde_json::json!({ "upstream": "127.0.0.1:8080", "upstream_kind": "tcp" }),
640 );
641 assert_eq!(rule.source.file, PathBuf::new());
642 assert_eq!(rule.source.line, 0);
643 assert_eq!(rule.max_body_bytes_request, 8 * 1024 * 1024);
644 assert_eq!(rule.max_body_bytes_response, 8 * 1024 * 1024);
645 }
646
647 #[test]
648 fn raw_rule_full_populates_every_field() {
649 let raw = serde_json::json!({
650 "name": "api",
651 "listen": [":443", "0.0.0.0:80"],
652 "match": { "tls.sni": { "equals": "api.example.com" } },
653 "middleware_chain": [
654 { "use": "rate_limit", "args": { "rate": 100 } },
655 { "use": "jwt", "args": { "secret": "x" }, "on_error": "close" },
656 ],
657 "terminate": {
658 "type": "http_proxy",
659 "upstream": "127.0.0.1:8080",
660 "timeouts": { "connect": "5s" }
661 },
662 "source": { "file": "rules/30-api.json", "line": 14 },
663 });
664 let rule: RawRule = serde_json::from_value(raw).expect("parse full rule");
665 assert_eq!(rule.name, "api");
666 assert_eq!(rule.listen.len(), 2);
667 let check = match rule.match_predicate.as_ref().expect("match present") {
668 Predicate::Check(c) => c,
669 other => panic!("expected Check, got {other:?}"),
670 };
671 assert_eq!(check.path, FieldPath::TlsSni);
672 match &check.op {
673 Operator::Equals(PredValue::Str(s)) => assert_eq!(s, "api.example.com"),
674 other => panic!("unexpected op: {other:?}"),
675 }
676 assert_eq!(rule.middleware_chain.len(), 2);
677 assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
678 assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
679 assert_eq!(
680 rule.terminate.args,
681 serde_json::json!({
682 "upstream": "127.0.0.1:8080",
683 "upstream_kind": "tcp",
684 "timeouts": { "connect": "5s" }
685 }),
686 );
687 assert_eq!(rule.source.file, PathBuf::from("rules/30-api.json"));
688 assert_eq!(rule.source.line, 14);
689 }
690
691 #[test]
692 fn middleware_ref_flat_form_parses_name_and_args() {
693 let raw = serde_json::json!({ "use": "rate_limit", "args": { "rate": 100 } });
694 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
695 assert_eq!(m.name, "rate_limit");
696 assert_eq!(m.args, serde_json::json!({ "rate": 100 }));
697 assert!(m.on_error.is_none());
698 }
699
700 #[test]
701 fn middleware_ref_on_error_close_form() {
702 let raw = serde_json::json!({ "use": "jwt", "args": { "secret": "x" }, "on_error": "close" });
703 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
704 assert_eq!(m.on_error, Some(OnErrorSpec::Close));
705 }
706
707 #[test]
708 fn middleware_ref_on_error_response_object_form() {
709 let raw = serde_json::json!({
710 "use": "jwt",
711 "on_error": { "response": { "status": 503, "body": "maintenance" } },
712 });
713 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
714 assert_eq!(m.name, "jwt");
715 assert_eq!(m.args, Value::Null);
716 let resp = match m.on_error.expect("on_error present") {
717 OnErrorSpec::Response(r) => r,
718 OnErrorSpec::Close => panic!("expected Response"),
719 };
720 assert_eq!(resp.status, 503);
721 assert_eq!(resp.body.as_deref(), Some("maintenance"));
722 assert!(resp.headers.is_none());
723 }
724
725 #[test]
726 fn middleware_ref_args_defaults_to_null_when_omitted() {
727 let raw = serde_json::json!({ "use": "tag" });
728 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
729 assert_eq!(m.args, Value::Null);
730 }
731
732 #[test]
733 fn middleware_ref_requires_use_key() {
734 let raw = serde_json::json!({});
735 let err = serde_json::from_value::<MiddlewareRef>(raw).expect_err("missing `use` must fail");
736 let _ = err.to_string();
737 }
738
739 #[test]
740 fn on_error_spec_string_invalid_variant_rejected() {
741 let raw = serde_json::json!("crash");
742 let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("non-`close` literal rejected");
743 let msg = err.to_string();
744 assert!(msg.contains("close"), "error names the only valid literal: {msg}");
745 }
746
747 #[test]
748 fn on_error_spec_malformed_response_object_rejected() {
749 let raw = serde_json::json!({ "response": null });
750 let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("null response rejected");
751 let _ = err.to_string();
752 }
753
754 #[test]
755 fn on_error_spec_close_literal_parses() {
756 let raw = serde_json::json!("close");
757 let s: OnErrorSpec = serde_json::from_value(raw).expect("close literal parses");
758 assert_eq!(s, OnErrorSpec::Close);
759 }
760
761 #[test]
762 fn on_error_spec_response_object_parses() {
763 let raw = serde_json::json!({
764 "response": { "status": 503, "body": "maintenance" },
765 });
766 let s: OnErrorSpec = serde_json::from_value(raw).expect("response object parses");
767 match s {
768 OnErrorSpec::Response(r) => {
769 assert_eq!(r.status, 503);
770 assert_eq!(r.body.as_deref(), Some("maintenance"));
771 assert!(r.headers.is_none());
772 }
773 OnErrorSpec::Close => panic!("expected Response"),
774 }
775 }
776
777 #[test]
778 fn synth_response_minimal_status_only() {
779 let raw = serde_json::json!({ "status": 200 });
780 let r: SynthResponse = serde_json::from_value(raw).expect("parse status-only synth");
781 assert_eq!(r.status, 200);
782 assert!(r.headers.is_none());
783 assert!(r.body.is_none());
784 }
785
786 #[test]
787 fn synth_response_full_status_headers_body() {
788 let raw = serde_json::json!({
789 "status": 404,
790 "headers": { "content-type": "text/plain" },
791 "body": "not found",
792 });
793 let r: SynthResponse = serde_json::from_value(raw).expect("parse full synth");
794 assert_eq!(r.status, 404);
795 let headers = r.headers.as_ref().expect("headers present");
796 assert_eq!(headers.get("content-type").map(String::as_str), Some("text/plain"));
797 assert_eq!(r.body.as_deref(), Some("not found"));
798 }
799
800 #[test]
801 fn terminate_spec_alias_table_maps_to_fetch_kind() {
802 let cases: &[(&str, FetchKind)] = &[
804 ("tcp_forward", FetchKind::L4Forward),
805 ("udp_forward", FetchKind::L4Forward),
806 ("http_proxy", FetchKind::HttpProxy),
807 ("http1_proxy", FetchKind::HttpProxy),
808 ("http2_proxy", FetchKind::HttpProxy),
809 ("http3_proxy", FetchKind::HttpProxy),
810 ("unix_proxy", FetchKind::HttpProxy),
811 ("cgi", FetchKind::HttpProxy),
812 ("websocket", FetchKind::WebSocketUpgrade),
813 ("static", FetchKind::HttpSynthesize),
814 ("redirect_https", FetchKind::HttpSynthesize),
815 ];
816 for (alias, expected) in cases {
817 let raw = serde_json::json!({ "type": alias });
818 let t: TerminateSpec =
819 serde_json::from_value(raw).unwrap_or_else(|e| panic!("alias {alias} must parse: {e}"));
820 assert_eq!(t.kind, *expected, "alias {alias} must map to {expected:?}");
821 }
822 }
823
824 #[test]
825 fn terminate_spec_args_preserves_all_non_type_keys_verbatim() {
826 let raw = serde_json::json!({
829 "type": "http_proxy",
830 "upstream": "127.0.0.1:8080",
831 "timeouts": { "connect": "5s", "total": "60s" },
832 });
833 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
834 assert_eq!(t.kind, FetchKind::HttpProxy);
835 assert_eq!(
836 t.args,
837 serde_json::json!({
838 "upstream": "127.0.0.1:8080",
839 "upstream_kind": "tcp",
840 "timeouts": { "connect": "5s", "total": "60s" },
841 }),
842 );
843 }
844
845 #[test]
846 fn terminate_spec_udp_forward_alias_injects_transport_udp() {
847 let raw = serde_json::json!({ "type": "udp_forward", "upstream": "1.2.3.4:53" });
848 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
849 assert_eq!(t.kind, FetchKind::L4Forward);
850 assert_eq!(t.args["transport"], "udp");
851 assert_eq!(t.args["upstream"], "1.2.3.4:53");
852 }
853
854 #[test]
855 fn terminate_spec_tcp_forward_alias_injects_transport_tcp() {
856 let raw = serde_json::json!({ "type": "tcp_forward", "upstream": "10.0.0.5:22" });
857 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
858 assert_eq!(t.kind, FetchKind::L4Forward);
859 assert_eq!(t.args["transport"], "tcp");
860 }
861
862 #[test]
863 fn terminate_spec_cgi_alias_injects_upstream_kind_cgi() {
864 let raw = serde_json::json!({ "type": "cgi", "binary": "/usr/bin/true" });
869 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
870 assert_eq!(t.kind, FetchKind::HttpProxy);
871 assert_eq!(t.args["upstream_kind"], "cgi");
872 }
873
874 #[test]
875 fn terminate_spec_http_proxy_aliases_inject_upstream_kind_tcp() {
876 for alias in ["http_proxy", "http1_proxy", "http2_proxy", "http3_proxy", "unix_proxy"] {
881 let raw = serde_json::json!({ "type": alias, "upstream": "127.0.0.1:8080" });
882 let t: TerminateSpec =
883 serde_json::from_value(raw).unwrap_or_else(|e| panic!("alias {alias} must parse: {e}"));
884 assert_eq!(t.args["upstream_kind"], "tcp", "alias {alias} must inject upstream_kind: tcp");
885 }
886 }
887
888 #[test]
889 fn terminate_spec_explicit_upstream_kind_wins_over_alias() {
890 let raw = serde_json::json!({
894 "type": "http_proxy",
895 "upstream": "127.0.0.1:8080",
896 "upstream_kind": "tcp",
897 });
898 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
899 assert_eq!(t.args["upstream_kind"], "tcp");
900 }
901
902 #[test]
903 fn terminate_spec_explicit_transport_wins_over_alias() {
904 let raw = serde_json::json!({ "type": "udp_forward", "upstream": "x", "transport": "tcp" });
908 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
909 assert_eq!(t.args["transport"], "tcp");
910 }
911
912 #[test]
913 fn terminate_spec_alias_only_yields_object_with_injected_markers() {
914 let raw = serde_json::json!({ "type": "http_proxy" });
921 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
922 assert_eq!(t.kind, FetchKind::HttpProxy);
923 assert!(t.args.is_object(), "args must be an object, got {:?}", t.args);
924 assert_eq!(t.args["upstream_kind"], "tcp");
925 }
926
927 #[test]
928 fn terminate_spec_unknown_type_rejected_and_names_alias() {
929 let raw = serde_json::json!({ "type": "bogus" });
930 let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("unknown alias rejected");
931 assert!(err.to_string().contains("bogus"), "error must name the offending alias: {err}");
932 }
933
934 #[test]
935 fn terminate_spec_missing_type_rejected_and_names_field() {
936 let raw = serde_json::json!({ "upstream": "127.0.0.1:8080" });
937 let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("missing type rejected");
938 assert!(err.to_string().contains("type"), "error must name the missing field: {err}");
939 }
940
941 #[test]
942 fn source_info_default_is_empty_path_and_zero_line() {
943 let s = SourceInfo::default();
944 assert_eq!(s.file, PathBuf::new());
945 assert_eq!(s.line, 0);
946 }
947
948 #[test]
949 fn source_info_round_trip_via_json() {
950 let raw = serde_json::json!({ "file": "rules/a.json", "line": 7 });
951 let s: SourceInfo = serde_json::from_value(raw).expect("parse source info");
952 assert_eq!(s.file, PathBuf::from("rules/a.json"));
953 assert_eq!(s.line, 7);
954 }
955
956 #[test]
957 fn middleware_chain_defaults_to_empty_when_omitted() {
958 let raw = serde_json::json!({
959 "name": "r",
960 "listen": [":443"],
961 "terminate": { "type": "http_proxy" },
962 });
963 let rule: RawRule = serde_json::from_value(raw).expect("parse");
964 assert!(rule.middleware_chain.is_empty());
965 }
966
967 #[test]
968 fn middleware_ref_chain_mixes_on_error_forms() {
969 let raw = serde_json::json!({
970 "name": "r",
971 "listen": [":443"],
972 "middleware_chain": [
973 { "use": "a" },
974 { "use": "b", "on_error": "close" },
975 { "use": "c", "on_error": { "response": { "status": 500 } } },
976 ],
977 "terminate": { "type": "http_proxy" },
978 });
979 let rule: RawRule = serde_json::from_value(raw).expect("parse");
980 assert_eq!(rule.middleware_chain.len(), 3);
981 assert!(rule.middleware_chain[0].on_error.is_none());
982 assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
983 match rule.middleware_chain[2].on_error.as_ref().expect("on_error[2]") {
984 OnErrorSpec::Response(r) => {
985 assert_eq!(r.status, 500);
986 assert!(r.body.is_none());
987 assert!(r.headers.is_none());
988 }
989 OnErrorSpec::Close => panic!("expected Response at index 2"),
990 }
991 }
992
993 #[test]
994 fn raw_rule_accepts_top_level_check_predicate() {
995 let raw = serde_json::json!({
996 "name": "r",
997 "listen": [":80"],
998 "match": { "http.uri.path": { "prefix": "/api" } },
999 "terminate": { "type": "http_proxy" },
1000 });
1001 let rule: RawRule = serde_json::from_value(raw).expect("parse");
1002 let Some(Predicate::Check(CheckMap { path, op })) = rule.match_predicate else {
1003 panic!("expected Check predicate");
1004 };
1005 assert_eq!(path, FieldPath::HttpUriPath);
1006 match op {
1007 Operator::Prefix(PredValue::Str(s)) => assert_eq!(s, "/api"),
1008 other => panic!("unexpected op: {other:?}"),
1009 }
1010 }
1011
1012 #[test]
1013 fn raw_rule_without_tls_field_defaults_to_none() {
1014 let raw = serde_json::json!({
1015 "name": "r",
1016 "listen": [":80"],
1017 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
1018 });
1019 let rule: RawRule = serde_json::from_value(raw).expect("parse rule without tls");
1020 assert!(rule.tls.is_none());
1021 }
1022
1023 #[test]
1024 fn raw_rule_with_tls_field_parses_paths() {
1025 let raw = serde_json::json!({
1026 "name": "r",
1027 "listen": [":443"],
1028 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
1029 "tls": {
1030 "cert_file": "/etc/vaned/certs/api.pem",
1031 "key_file": "/etc/vaned/certs/api.key",
1032 "enable_zero_rtt": false,
1033 },
1034 });
1035 let rule: RawRule = serde_json::from_value(raw).expect("parse rule with tls");
1036 let tls = rule.tls.expect("tls present");
1037 assert_eq!(tls.cert_file.as_deref(), Some(Path::new("/etc/vaned/certs/api.pem")));
1038 assert_eq!(tls.key_file.as_deref(), Some(Path::new("/etc/vaned/certs/api.key")));
1039 assert!(!tls.enable_zero_rtt);
1040 }
1041
1042 #[test]
1043 fn tls_config_round_trips_through_json() {
1044 let original = TlsConfig {
1045 sni: None,
1046 cert_file: Some(PathBuf::from("/srv/cert.pem")),
1047 key_file: Some(PathBuf::from("/srv/key.pem")),
1048 managed: None,
1049 enable_zero_rtt: false,
1050 client_auth: None,
1051 ocsp_path: None,
1052 ocsp_fetch: false,
1053 };
1054 let encoded = serde_json::to_string(&original).expect("serialize");
1055 let decoded: TlsConfig = serde_json::from_str(&encoded).expect("deserialize");
1056 assert_eq!(decoded, original);
1057 }
1058
1059 #[test]
1060 fn tls_config_with_sni_field_parses() {
1061 let raw = serde_json::json!({
1062 "sni": "api.example.com",
1063 "cert_file": "/etc/vaned/certs/api.pem",
1064 "key_file": "/etc/vaned/certs/api.key",
1065 "enable_zero_rtt": false,
1066 });
1067 let tls: TlsConfig = serde_json::from_value(raw).expect("parse tls with sni");
1068 assert_eq!(tls.sni.as_deref(), Some("api.example.com"));
1069 }
1070
1071 #[test]
1072 fn tls_config_without_sni_parses_with_none() {
1073 let raw = serde_json::json!({
1074 "cert_file": "/etc/vaned/certs/default.pem",
1075 "key_file": "/etc/vaned/certs/default.key",
1076 "enable_zero_rtt": false,
1077 });
1078 let tls: TlsConfig = serde_json::from_value(raw).expect("parse tls without sni");
1079 assert!(tls.sni.is_none());
1080 }
1081
1082 #[test]
1083 fn tls_config_missing_enable_zero_rtt_field_rejected() {
1084 let raw = serde_json::json!({
1088 "cert_file": "/etc/vaned/certs/default.pem",
1089 "key_file": "/etc/vaned/certs/default.key",
1090 });
1091 let err =
1092 serde_json::from_value::<TlsConfig>(raw).expect_err("missing enable_zero_rtt must reject");
1093 assert!(
1094 err.to_string().contains("enable_zero_rtt"),
1095 "error must name the missing field: {err}",
1096 );
1097 }
1098
1099 #[test]
1100 fn raw_rule_allow_zero_rtt_field_parses_when_present() {
1101 let raw = serde_json::json!({
1102 "name": "r",
1103 "listen": [":443"],
1104 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
1105 "allow_zero_rtt": true,
1106 "tls": {
1107 "cert_file": "/etc/vaned/certs/api.pem",
1108 "key_file": "/etc/vaned/certs/api.key",
1109 "enable_zero_rtt": true,
1110 },
1111 });
1112 let rule: RawRule = serde_json::from_value(raw).expect("parse rule with allow_zero_rtt");
1113 assert_eq!(rule.allow_zero_rtt, Some(true));
1114 }
1115
1116 #[test]
1117 fn raw_rule_allow_zero_rtt_defaults_to_none_when_omitted() {
1118 let raw = serde_json::json!({
1119 "name": "r",
1120 "listen": [":80"],
1121 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
1122 });
1123 let rule: RawRule = serde_json::from_value(raw).expect("parse rule without allow_zero_rtt");
1124 assert!(rule.allow_zero_rtt.is_none());
1125 }
1126
1127 fn managed_tls(challenge: &str, with_dns_provider: bool) -> serde_json::Value {
1135 let mut managed = serde_json::json!({
1136 "directory_url": "https://acme-staging-v02.api.letsencrypt.org/directory",
1137 "contact": ["mailto:ops@example.com"],
1138 "agree_tos": true,
1139 "challenge": challenge,
1140 "key_type": "ecdsa-p256",
1141 "renew_before": "30d",
1142 "san": ["api.example.com"],
1143 });
1144 if with_dns_provider {
1145 managed["dns_provider"] = serde_json::json!({ "kind": "cloudflare" });
1146 }
1147 serde_json::json!({
1148 "sni": "api.example.com",
1149 "managed": managed,
1150 "enable_zero_rtt": false,
1151 })
1152 }
1153
1154 #[test]
1155 fn tls_managed_round_trips_through_json() {
1156 let raw = managed_tls("http-01", false);
1157 let tls: TlsConfig = serde_json::from_value(raw).expect("parse managed");
1158 let m = tls.managed.as_ref().expect("managed");
1159 assert!(m.agree_tos);
1160 assert_eq!(m.challenge, ChallengeKind::Http01);
1161 assert_eq!(m.key_type, ManagedKeyType::EcdsaP256);
1162 assert_eq!(m.san, vec!["api.example.com".to_owned()]);
1163 assert_eq!(m.contact, vec!["mailto:ops@example.com".to_owned()]);
1164 assert!(m.dns_provider.is_none());
1165 assert!(tls.is_managed());
1166 assert!(!tls.is_static());
1167 }
1168
1169 #[test]
1170 fn tls_managed_validates_happy_path() {
1171 let raw = managed_tls("http-01", false);
1172 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1173 tls.validate().expect("happy path validates");
1174 }
1175
1176 #[test]
1177 fn tls_validate_rejects_both_static_and_managed() {
1178 let raw = serde_json::json!({
1179 "sni": "api.example.com",
1180 "cert_file": "/tmp/cert.pem",
1181 "key_file": "/tmp/key.pem",
1182 "managed": {
1183 "directory_url": "https://example",
1184 "contact": ["mailto:ops@example.com"],
1185 "agree_tos": true,
1186 "challenge": "http-01",
1187 "key_type": "ecdsa-p256",
1188 "renew_before": "30d",
1189 "san": ["api.example.com"],
1190 },
1191 "enable_zero_rtt": false,
1192 });
1193 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1194 let err = tls.validate().expect_err("must reject");
1195 assert!(err.to_string().contains("must not coexist"), "{err}");
1196 }
1197
1198 #[test]
1199 fn tls_validate_rejects_neither_static_nor_managed() {
1200 let raw = serde_json::json!({ "enable_zero_rtt": false });
1201 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1202 let err = tls.validate().expect_err("must reject");
1203 assert!(err.to_string().contains("missing cert source"), "{err}");
1204 }
1205
1206 #[test]
1207 fn tls_validate_rejects_partial_static_paths() {
1208 let raw = serde_json::json!({
1209 "cert_file": "/tmp/cert.pem",
1210 "enable_zero_rtt": false,
1211 });
1212 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1213 let err = tls.validate().expect_err("must reject");
1214 assert!(err.to_string().contains("`key_file`"), "{err}");
1215 }
1216
1217 #[test]
1218 fn tls_managed_rejects_agree_tos_false() {
1219 let mut raw = managed_tls("http-01", false);
1220 raw["managed"]["agree_tos"] = serde_json::Value::Bool(false);
1221 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1222 let err = tls.validate().expect_err("must reject");
1223 assert!(err.to_string().contains("agree_tos must be true"), "{err}");
1224 }
1225
1226 #[test]
1227 fn tls_managed_rejects_dns01_without_dns_provider() {
1228 let raw = managed_tls("dns-01", false);
1229 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1230 let err = tls.validate().expect_err("must reject");
1231 assert!(err.to_string().contains("requires `dns_provider`"), "{err}");
1232 }
1233
1234 #[test]
1235 fn tls_managed_rejects_http01_with_dns_provider() {
1236 let raw = managed_tls("http-01", true);
1237 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1238 let err = tls.validate().expect_err("must reject");
1239 assert!(err.to_string().contains("dns_provider"), "{err}");
1240 }
1241
1242 #[test]
1243 fn tls_managed_rejects_wildcard_san_with_http01() {
1244 let mut raw = managed_tls("http-01", false);
1245 raw["managed"]["san"] = serde_json::json!(["*.example.com", "api.example.com"]);
1246 raw["sni"] = serde_json::Value::String("api.example.com".to_owned());
1247 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1248 let err = tls.validate().expect_err("must reject");
1249 assert!(err.to_string().contains("wildcard"), "{err}");
1250 }
1251
1252 #[test]
1253 fn tls_managed_accepts_wildcard_san_with_dns01() {
1254 let mut raw = managed_tls("dns-01", true);
1255 raw["managed"]["san"] = serde_json::json!(["*.example.com", "api.example.com"]);
1256 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1257 tls.validate().expect("dns-01 wildcard ok");
1258 }
1259
1260 #[test]
1261 fn tls_managed_rejects_san_missing_sni() {
1262 let mut raw = managed_tls("http-01", false);
1263 raw["sni"] = serde_json::Value::String("other.example.com".to_owned());
1264 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1265 let err = tls.validate().expect_err("must reject");
1266 assert!(err.to_string().contains("must contain tls.sni"), "{err}");
1267 }
1268
1269 #[test]
1270 fn tls_managed_rejects_missing_sni() {
1271 let mut raw = managed_tls("http-01", false);
1272 raw.as_object_mut().expect("obj").remove("sni");
1273 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1274 let err = tls.validate().expect_err("must reject");
1275 assert!(err.to_string().contains("requires tls.sni"), "{err}");
1276 }
1277
1278 #[test]
1279 fn tls_managed_rejects_empty_contact() {
1280 let mut raw = managed_tls("http-01", false);
1281 raw["managed"]["contact"] = serde_json::json!([]);
1282 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1283 let err = tls.validate().expect_err("must reject");
1284 assert!(err.to_string().contains("contact must list"), "{err}");
1285 }
1286
1287 #[test]
1288 fn tls_managed_rejects_empty_san() {
1289 let mut raw = managed_tls("http-01", false);
1290 raw["managed"]["san"] = serde_json::json!([]);
1291 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1292 let err = tls.validate().expect_err("must reject");
1293 assert!(err.to_string().contains("san must list"), "{err}");
1294 }
1295
1296 #[test]
1297 fn tls_managed_rejects_empty_directory_url() {
1298 let mut raw = managed_tls("http-01", false);
1299 raw["managed"]["directory_url"] = serde_json::Value::String(String::new());
1300 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1301 let err = tls.validate().expect_err("must reject");
1302 assert!(err.to_string().contains("directory_url"), "{err}");
1303 }
1304
1305 #[test]
1306 fn tls_managed_rejects_zero_renew_before() {
1307 let mut raw = managed_tls("http-01", false);
1308 raw["managed"]["renew_before"] = serde_json::Value::String("0d".to_owned());
1309 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1310 let err = tls.validate().expect_err("must reject");
1311 assert!(err.to_string().contains("must be > 0"), "{err}");
1312 }
1313
1314 #[test]
1315 fn tls_managed_rejects_unparseable_renew_before() {
1316 let mut raw = managed_tls("http-01", false);
1317 raw["managed"]["renew_before"] = serde_json::Value::String("garbage".to_owned());
1318 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1319 let err = tls.validate().expect_err("must reject");
1320 assert!(err.to_string().contains("missing unit"), "{err}");
1321 }
1322
1323 #[test]
1324 fn renewal_duration_handles_h_d_units() {
1325 assert_eq!(parse_renewal_duration("30d").unwrap(), Duration::from_hours(720));
1326 assert_eq!(parse_renewal_duration("12h").unwrap(), Duration::from_hours(12));
1327 assert_eq!(parse_renewal_duration("90s").unwrap(), Duration::from_secs(90));
1328 assert_eq!(parse_renewal_duration("500ms").unwrap(), Duration::from_millis(500));
1329 }
1330
1331 #[test]
1332 fn tls_managed_serializes_omitting_optional_fields() {
1333 let raw = managed_tls("http-01", false);
1334 let tls: TlsConfig = serde_json::from_value(raw).expect("parse");
1335 let json = serde_json::to_value(&tls).expect("serialize");
1336 assert!(json.as_object().expect("obj").get("cert_file").is_none());
1338 assert!(json.as_object().expect("obj").get("key_file").is_none());
1339 assert!(json["managed"].as_object().expect("managed obj").get("dns_provider").is_none());
1341 }
1342
1343 #[test]
1344 fn challenge_kind_round_trips_kebab_case() {
1345 assert_eq!(serde_json::to_string(&ChallengeKind::Http01).expect("ser"), "\"http-01\"");
1346 assert_eq!(serde_json::to_string(&ChallengeKind::Dns01).expect("ser"), "\"dns-01\"");
1347 let parsed: ChallengeKind = serde_json::from_str("\"http-01\"").expect("de");
1348 assert_eq!(parsed, ChallengeKind::Http01);
1349 }
1350
1351 #[test]
1352 fn key_type_round_trips_kebab_case() {
1353 assert_eq!(serde_json::to_string(&ManagedKeyType::EcdsaP256).expect("ser"), "\"ecdsa-p256\"");
1354 assert_eq!(serde_json::to_string(&ManagedKeyType::Rsa2048).expect("ser"), "\"rsa-2048\"");
1355 }
1356}