Skip to main content

rskit_server/
http_config.rs

1use std::time::Duration;
2
3pub use rskit_http::CorsPolicy;
4use rskit_security::TlsConfig;
5use serde::Deserialize;
6use validator::{Validate, ValidationError, ValidationErrors};
7
8/// HTTP server configuration owned by `rskit-server`.
9#[derive(Debug, Clone, Deserialize)]
10pub struct HttpServerConfig {
11    /// Bind address (default: `0.0.0.0`).
12    #[serde(default = "HttpServerConfig::default_host")]
13    pub host: String,
14
15    /// Bind port (default: `8080`).
16    #[serde(default = "HttpServerConfig::default_port")]
17    pub port: u16,
18
19    /// Maximum time to wait for a request header (default: 30 s).
20    #[serde(default = "HttpServerConfig::default_timeout")]
21    pub read_timeout: Duration,
22
23    /// Idle keep-alive connection timeout (default: 60 s).
24    #[serde(default = "HttpServerConfig::default_idle_timeout")]
25    pub idle_timeout: Duration,
26
27    /// End-to-end request timeout enforced by the HTTP middleware stack (default: 30 s).
28    #[serde(default = "HttpServerConfig::default_timeout")]
29    pub request_timeout: Duration,
30
31    /// Maximum accepted request body size in bytes (default: 2 MiB).
32    #[serde(default = "HttpServerConfig::default_max_body_bytes")]
33    pub max_body_bytes: usize,
34
35    /// Enable HTTP/2 cleartext (h2c) on the same port (default: `true`).
36    #[serde(default = "HttpServerConfig::default_h2c")]
37    pub enable_h2c: bool,
38
39    /// Optional CORS policy.
40    pub cors: Option<CorsPolicy>,
41
42    /// Optional direct HTTPS serving configuration.
43    ///
44    /// When configured, rustls is used with TLS 1.3 preferred and TLS 1.2 as
45    /// the minimum protocol floor unless a stricter minimum is configured.
46    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    /// Returns the bind address as `host:port` (or `[host]:port` for IPv6).
140    #[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}