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