1use rskit_errors::{AppError, AppResult};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]
7#[non_exhaustive]
8pub enum TlsVersion {
9 #[default]
11 Tls12,
12 Tls13,
14}
15
16#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
18pub struct TlsConfig {
19 pub skip_verify: bool,
21 pub ca_file: Option<String>,
23 pub cert_file: Option<String>,
25 pub key_file: Option<String>,
27 pub server_name: Option<String>,
29 #[serde(default)]
31 pub min_version: TlsVersion,
32}
33
34impl TlsConfig {
35 #[must_use]
37 pub fn is_enabled(&self) -> bool {
38 self.skip_verify
39 || self.ca_file.as_ref().is_some_and(|value| !value.is_empty())
40 || self
41 .cert_file
42 .as_ref()
43 .is_some_and(|value| !value.is_empty())
44 || self
45 .key_file
46 .as_ref()
47 .is_some_and(|value| !value.is_empty())
48 || self
49 .server_name
50 .as_ref()
51 .is_some_and(|value| !value.is_empty())
52 || self.min_version != TlsVersion::Tls12
53 }
54
55 pub fn validate(&self) -> AppResult<()> {
60 validate_non_empty("ca_file", self.ca_file.as_deref())?;
61 validate_non_empty("cert_file", self.cert_file.as_deref())?;
62 validate_non_empty("key_file", self.key_file.as_deref())?;
63 validate_non_empty("server_name", self.server_name.as_deref())?;
64
65 if self.cert_file.is_some() != self.key_file.is_some() {
66 return Err(AppError::invalid_input(
67 "tls",
68 "cert_file and key_file must be provided together",
69 ));
70 }
71 Ok(())
72 }
73}
74
75fn validate_non_empty(field: &str, value: Option<&str>) -> AppResult<()> {
76 if value.is_some_and(str::is_empty) {
77 return Err(AppError::invalid_input(
78 field,
79 format!("{field} must not be empty when provided"),
80 ));
81 }
82 Ok(())
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn cert_and_key_must_be_configured_together() {
91 let config = TlsConfig {
92 cert_file: Some("client.pem".to_string()),
93 ..Default::default()
94 };
95 assert!(config.validate().is_err());
96 }
97
98 #[test]
99 fn detects_enabled_tls_settings() {
100 let config = TlsConfig {
101 ca_file: Some("ca.pem".to_string()),
102 ..Default::default()
103 };
104 assert!(config.is_enabled());
105 }
106
107 #[test]
108 fn default_config_is_disabled_and_valid() {
109 let config = TlsConfig::default();
110
111 assert!(!config.is_enabled());
112 assert!(config.validate().is_ok());
113 }
114
115 #[test]
116 fn cert_and_key_pair_is_valid() {
117 let config = TlsConfig {
118 cert_file: Some("client.pem".to_string()),
119 key_file: Some("client.key".to_string()),
120 ..Default::default()
121 };
122
123 assert!(config.validate().is_ok());
124 assert!(config.is_enabled());
125 }
126
127 #[test]
128 fn rejects_empty_optional_values() {
129 for config in [
130 TlsConfig {
131 ca_file: Some(String::new()),
132 ..Default::default()
133 },
134 TlsConfig {
135 cert_file: Some(String::new()),
136 key_file: Some("key.pem".to_string()),
137 ..Default::default()
138 },
139 TlsConfig {
140 key_file: Some(String::new()),
141 cert_file: Some("cert.pem".to_string()),
142 ..Default::default()
143 },
144 TlsConfig {
145 server_name: Some(String::new()),
146 ..Default::default()
147 },
148 ] {
149 assert!(config.validate().is_err());
150 }
151 }
152
153 #[test]
154 fn tls13_minimum_counts_as_enabled() {
155 let config = TlsConfig {
156 min_version: TlsVersion::Tls13,
157 ..Default::default()
158 };
159
160 assert!(config.is_enabled());
161 assert!(config.validate().is_ok());
162 }
163}