rskit_server/
http_config.rs1use std::time::Duration;
2
3pub use rskit_http::CorsPolicy;
4use rskit_security::TlsConfig;
5use serde::Deserialize;
6use validator::{Validate, ValidationError, ValidationErrors};
7
8#[derive(Debug, Clone, Deserialize)]
10pub struct HttpServerConfig {
11 #[serde(default = "HttpServerConfig::default_host")]
13 pub host: String,
14
15 #[serde(default = "HttpServerConfig::default_port")]
17 pub port: u16,
18
19 #[serde(default = "HttpServerConfig::default_timeout")]
21 pub read_timeout: Duration,
22
23 #[serde(default = "HttpServerConfig::default_idle_timeout")]
25 pub idle_timeout: Duration,
26
27 #[serde(default = "HttpServerConfig::default_timeout")]
29 pub request_timeout: Duration,
30
31 #[serde(default = "HttpServerConfig::default_max_body_bytes")]
33 pub max_body_bytes: usize,
34
35 #[serde(default = "HttpServerConfig::default_h2c")]
37 pub enable_h2c: bool,
38
39 pub cors: Option<CorsPolicy>,
41
42 pub tls: Option<TlsConfig>,
47}
48
49impl Validate for HttpServerConfig {
50 fn validate(&self) -> Result<(), ValidationErrors> {
51 let mut errors = ValidationErrors::new();
52
53 if self.port == 0 {
54 errors.add("port", ValidationError::new("range"));
55 }
56
57 if let Some(cors) = &self.cors
58 && let Err(error) = cors.validate()
59 {
60 let mut validation_error = ValidationError::new("invalid_cors");
61 validation_error.message = Some(error.to_string().into());
62 errors.add("cors", validation_error);
63 }
64
65 if let Some(tls) = &self.tls
66 && let Err(error) = validate_http_tls_config(tls)
67 {
68 let mut validation_error = ValidationError::new("invalid_tls");
69 validation_error.message = Some(error.to_string().into());
70 errors.add("tls", validation_error);
71 }
72
73 if errors.is_empty() {
74 Ok(())
75 } else {
76 Err(errors)
77 }
78 }
79}
80
81pub(crate) fn validate_http_tls_config(tls: &TlsConfig) -> rskit_errors::AppResult<()> {
82 tls.validate()?;
83 if tls.cert_file.is_none() || tls.key_file.is_none() {
84 return Err(rskit_errors::AppError::invalid_input(
85 "tls",
86 "tls.cert_file and tls.key_file are required for HTTPS serving",
87 ));
88 }
89 if tls.skip_verify || tls.ca_file.is_some() || tls.server_name.is_some() {
90 return Err(rskit_errors::AppError::invalid_input(
91 "tls",
92 "skip_verify, ca_file, and server_name are client-side TLS settings and are not used by HTTPS serving",
93 ));
94 }
95 Ok(())
96}
97
98impl Default for HttpServerConfig {
99 fn default() -> Self {
100 Self {
101 host: Self::default_host(),
102 port: Self::default_port(),
103 read_timeout: Self::default_timeout(),
104 idle_timeout: Self::default_idle_timeout(),
105 request_timeout: Self::default_timeout(),
106 max_body_bytes: Self::default_max_body_bytes(),
107 enable_h2c: Self::default_h2c(),
108 cors: None,
109 tls: None,
110 }
111 }
112}
113
114impl HttpServerConfig {
115 fn default_host() -> String {
116 "0.0.0.0".to_string()
117 }
118
119 fn default_port() -> u16 {
120 8080
121 }
122
123 fn default_timeout() -> Duration {
124 Duration::from_secs(30)
125 }
126
127 fn default_idle_timeout() -> Duration {
128 Duration::from_secs(60)
129 }
130
131 fn default_max_body_bytes() -> usize {
132 2 * 1024 * 1024
133 }
134
135 fn default_h2c() -> bool {
136 true
137 }
138
139 #[must_use]
141 pub fn bind_addr(&self) -> String {
142 if self.host.contains(':') && !self.host.starts_with('[') {
143 format!("[{}]:{}", self.host, self.port)
144 } else {
145 format!("{}:{}", self.host, self.port)
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use rskit_errors::ErrorCode;
153 use rskit_security::TlsVersion;
154
155 use super::*;
156
157 #[test]
158 fn bind_addr_wraps_ipv6_and_validation_rejects_invalid_port() {
159 let config = HttpServerConfig {
160 host: "::1".to_string(),
161 port: 443,
162 ..Default::default()
163 };
164 assert_eq!(config.bind_addr(), "[::1]:443");
165 assert_eq!(HttpServerConfig::default().bind_addr(), "0.0.0.0:8080");
166
167 let invalid = HttpServerConfig {
168 port: 0,
169 ..Default::default()
170 };
171 assert!(invalid.validate().is_err());
172 }
173
174 #[test]
175 fn validate_http_tls_rejects_missing_and_client_side_fields() {
176 let missing = TlsConfig::default();
177 assert_eq!(
178 validate_http_tls_config(&missing).unwrap_err().code(),
179 ErrorCode::InvalidInput
180 );
181
182 let client_side = TlsConfig {
183 cert_file: Some("cert.pem".to_string()),
184 key_file: Some("key.pem".to_string()),
185 skip_verify: true,
186 min_version: TlsVersion::Tls12,
187 ..Default::default()
188 };
189 let error = validate_http_tls_config(&client_side).unwrap_err();
190 assert_eq!(error.code(), ErrorCode::InvalidInput);
191 assert!(error.to_string().contains("client-side TLS settings"));
192 }
193
194 #[test]
195 fn http_config_validation_reports_invalid_cors_and_tls_branches() {
196 let invalid_cors = HttpServerConfig {
197 cors: Some(CorsPolicy {
198 allow_credentials: true,
199 ..Default::default()
200 }),
201 ..Default::default()
202 };
203 let errors = invalid_cors.validate().unwrap_err();
204 assert!(errors.field_errors().contains_key("cors"));
205
206 let invalid_tls = HttpServerConfig {
207 tls: Some(TlsConfig {
208 cert_file: Some("cert.pem".to_string()),
209 key_file: None,
210 min_version: TlsVersion::Tls12,
211 ..Default::default()
212 }),
213 ..Default::default()
214 };
215 let errors = invalid_tls.validate().unwrap_err();
216 assert!(errors.field_errors().contains_key("tls"));
217
218 let valid_tls = HttpServerConfig {
219 tls: Some(TlsConfig {
220 cert_file: Some("cert.pem".to_string()),
221 key_file: Some("key.pem".to_string()),
222 min_version: TlsVersion::Tls12,
223 ..Default::default()
224 }),
225 ..Default::default()
226 };
227 valid_tls.validate().unwrap();
228 validate_http_tls_config(valid_tls.tls.as_ref().unwrap()).unwrap();
229 }
230}