Skip to main content

gurty_cli/
config.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::PathBuf;
4use std::sync::Arc;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct GurtConfig {
9    pub server: ServerConfig,
10    pub tls: Option<TlsConfig>,
11    pub logging: Option<LoggingConfig>,
12    pub security: Option<SecurityConfig>,
13    pub error_pages: Option<ErrorPagesConfig>,
14    pub headers: Option<HashMap<String, String>>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ServerConfig {
19    #[serde(default = "default_host")]
20    pub host: String,
21    
22    #[serde(default = "default_port")]
23    pub port: u16,
24    
25    #[serde(default = "default_protocol_version")]
26    pub protocol_version: String,
27    
28    #[serde(default = "default_alpn_identifier")]
29    pub alpn_identifier: String,
30    
31    pub timeouts: Option<TimeoutsConfig>,
32    
33    #[serde(default = "default_max_connections")]
34    pub max_connections: u32,
35    
36    #[serde(default = "default_max_message_size")]
37    pub max_message_size: String,
38    
39    #[serde(skip)]
40    pub base_directory: Arc<PathBuf>,
41    
42    #[serde(skip)]
43    pub verbose: bool,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TimeoutsConfig {
48    #[serde(default = "default_handshake_timeout")]
49    pub handshake: u64,
50    
51    #[serde(default = "default_request_timeout")]
52    pub request: u64,
53    
54    #[serde(default = "default_connection_timeout")]
55    pub connection: u64,
56    
57    #[serde(default = "default_pool_idle_timeout")]
58    pub pool_idle: u64,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct TlsConfig {
63    pub certificate: PathBuf,
64    pub private_key: PathBuf,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct LoggingConfig {
69    #[serde(default = "default_log_level")]
70    pub level: String,
71    
72    pub access_log: Option<PathBuf>,
73    pub error_log: Option<PathBuf>,
74    
75    #[serde(default = "default_log_requests")]
76    pub log_requests: bool,
77    
78    #[serde(default)]
79    pub log_responses: bool,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SecurityConfig {
84    #[serde(default)]
85    pub deny_files: Vec<String>,
86    
87    #[serde(default = "default_allowed_methods")]
88    pub allowed_methods: Vec<String>,
89    
90    #[serde(default = "default_rate_limit_requests")]
91    pub rate_limit_requests: u32,
92    
93    #[serde(default = "default_rate_limit_connections")]
94    pub rate_limit_connections: u32,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ErrorPagesConfig {
99    #[serde(flatten)]
100    pub pages: HashMap<String, String>,
101    
102    pub default: Option<ErrorPageDefaults>,
103}
104
105impl ErrorPagesConfig {
106    pub fn get_page(&self, status_code: u16) -> Option<&String> {
107        let code_str = status_code.to_string();
108        self.pages.get(&code_str)
109    }
110    
111    pub fn get_default_page(&self, status_code: u16) -> Option<&String> {
112        if let Some(defaults) = &self.default {
113            let code_str = status_code.to_string();
114            defaults.pages.get(&code_str)
115        } else {
116            None
117        }
118    }
119    
120    pub fn get_any_page(&self, status_code: u16) -> Option<&String> {
121        self.get_page(status_code)
122            .or_else(|| self.get_default_page(status_code))
123    }
124
125    pub fn get_page_content(&self, status_code: u16, base_dir: &std::path::Path) -> Option<String> {
126        if let Some(page_value) = self.get_page(status_code) {
127            if page_value.starts_with('/') || page_value.starts_with("./") {
128                let file_path = if page_value.starts_with('/') {
129                    base_dir.join(&page_value[1..])
130                } else {
131                    base_dir.join(page_value)
132                };
133                
134                if let Ok(content) = std::fs::read_to_string(&file_path) {
135                    return Some(content);
136                } else {
137                    tracing::warn!("Failed to read error page file: {}", file_path.display());
138                    return None;
139                }
140            } else {
141                return Some(page_value.clone());
142            }
143        }
144        
145        if let Some(page_value) = self.get_default_page(status_code) {
146            return Some(page_value.clone());
147        }
148        
149        None
150    }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ErrorPageDefaults {
155    #[serde(flatten)]
156    pub pages: HashMap<String, String>,
157}
158
159fn default_host() -> String { "127.0.0.1".to_string() }
160fn default_port() -> u16 { 4878 }
161fn default_protocol_version() -> String { "1.0.0".to_string() }
162fn default_alpn_identifier() -> String { "GURT/1.0".to_string() }
163fn default_max_connections() -> u32 { 10 }
164fn default_max_message_size() -> String { "10MB".to_string() }
165fn default_handshake_timeout() -> u64 { 5 }
166fn default_request_timeout() -> u64 { 30 }
167fn default_connection_timeout() -> u64 { 10 }
168fn default_pool_idle_timeout() -> u64 { 300 }
169fn default_log_level() -> String { "info".to_string() }
170fn default_log_requests() -> bool { true }
171fn default_allowed_methods() -> Vec<String> {
172    vec!["GET".to_string(), "POST".to_string(), "PUT".to_string(), 
173         "DELETE".to_string(), "HEAD".to_string(), "OPTIONS".to_string(), "PATCH".to_string()]
174}
175fn default_rate_limit_requests() -> u32 { 100 }
176fn default_rate_limit_connections() -> u32 { 10 }
177
178impl Default for GurtConfig {
179    fn default() -> Self {
180        Self {
181            server: ServerConfig::default(),
182            tls: None,
183            logging: None,
184            security: None,
185            error_pages: None,
186            headers: None,
187        }
188    }
189}
190
191impl Default for ServerConfig {
192    fn default() -> Self {
193        Self {
194            host: default_host(),
195            port: default_port(),
196            protocol_version: default_protocol_version(),
197            alpn_identifier: default_alpn_identifier(),
198            timeouts: None,
199            max_connections: default_max_connections(),
200            max_message_size: default_max_message_size(),
201            base_directory: Arc::new(PathBuf::from(".")),
202            verbose: false,
203        }
204    }
205}
206
207impl GurtConfig {
208    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> crate::Result<Self> {
209        let content = std::fs::read_to_string(path)
210            .map_err(|e| crate::ServerError::InvalidConfiguration(format!("Failed to read config file: {}", e)))?;
211        
212        let config: GurtConfig = toml::from_str(&content)
213            .map_err(|e| crate::ServerError::InvalidConfiguration(format!("Failed to parse config file: {}", e)))?;
214        
215        Ok(config)
216    }
217
218    pub fn builder() -> GurtConfigBuilder {
219        GurtConfigBuilder::default()
220    }
221
222    pub fn address(&self) -> String {
223        format!("{}:{}", self.server.host, self.server.port)
224    }
225
226    pub fn max_message_size_bytes(&self) -> crate::Result<u64> {
227        parse_size(&self.server.max_message_size)
228    }
229
230    pub fn get_handshake_timeout(&self) -> Duration {
231        Duration::from_secs(
232            self.server.timeouts
233                .as_ref()
234                .map(|t| t.handshake)
235                .unwrap_or(default_handshake_timeout())
236        )
237    }
238
239    pub fn get_request_timeout(&self) -> Duration {
240        Duration::from_secs(
241            self.server.timeouts
242                .as_ref()
243                .map(|t| t.request)
244                .unwrap_or(default_request_timeout())
245        )
246    }
247
248    pub fn get_connection_timeout(&self) -> Duration {
249        Duration::from_secs(
250            self.server.timeouts
251                .as_ref()
252                .map(|t| t.connection)
253                .unwrap_or(default_connection_timeout())
254        )
255    }
256
257    pub fn should_deny_file(&self, file_path: &str) -> bool {
258        if let Some(security) = &self.security {
259            for pattern in &security.deny_files {
260                if matches_pattern(file_path, pattern) {
261                    return true;
262                }
263            }
264        }
265        false
266    }
267
268    pub fn is_method_allowed(&self, method: &str) -> bool {
269        if let Some(security) = &self.security {
270            security.allowed_methods.contains(&method.to_uppercase())
271        } else {
272            default_allowed_methods().contains(&method.to_uppercase())
273        }
274    }
275
276    pub fn default_with_directory(base_dir: PathBuf) -> Self {
277        let mut config = Self::default();
278        config.server.base_directory = Arc::new(base_dir);
279        config
280    }
281
282    pub fn from_toml(toml_content: &str, base_dir: PathBuf) -> crate::Result<Self> {
283        let mut config: GurtConfig = toml::from_str(toml_content)
284            .map_err(|e| crate::ServerError::InvalidConfiguration(format!("Failed to parse config: {}", e)))?;
285        
286        config.server.base_directory = Arc::new(base_dir);
287        Ok(config)
288    }
289
290    pub fn validate(&self) -> crate::Result<()> {
291        if !self.server.base_directory.exists() || !self.server.base_directory.is_dir() {
292            return Err(crate::ServerError::InvalidConfiguration(
293                format!("Invalid base directory: {}", self.server.base_directory.display())
294            ));
295        }
296
297        if let Some(tls) = &self.tls {
298            if !tls.certificate.exists() {
299                return Err(crate::ServerError::TlsConfiguration(
300                    format!("Certificate file does not exist: {}", tls.certificate.display())
301                ));
302            }
303            if !tls.private_key.exists() {
304                return Err(crate::ServerError::TlsConfiguration(
305                    format!("Private key file does not exist: {}", tls.private_key.display())
306                ));
307            }
308        }
309
310        Ok(())
311    }
312}
313
314#[derive(Default)]
315pub struct GurtConfigBuilder {
316    config: GurtConfig,
317}
318
319impl GurtConfigBuilder {
320    pub fn new() -> Self {
321        Self::default()
322    }
323
324    pub fn host<S: Into<String>>(mut self, host: S) -> Self {
325        self.config.server.host = host.into();
326        self
327    }
328
329    pub fn port(mut self, port: u16) -> Self {
330        self.config.server.port = port;
331        self
332    }
333
334    pub fn base_directory<P: Into<PathBuf>>(mut self, dir: P) -> Self {
335        self.config.server.base_directory = Arc::new(dir.into());
336        self
337    }
338
339    pub fn verbose(mut self, verbose: bool) -> Self {
340        self.config.server.verbose = verbose;
341        self
342    }
343
344    pub fn tls_config(mut self, cert_path: PathBuf, key_path: PathBuf) -> Self {
345        self.config.tls = Some(TlsConfig {
346            certificate: cert_path,
347            private_key: key_path,
348        });
349        self
350    }
351
352    pub fn logging_config(mut self, config: LoggingConfig) -> Self {
353        self.config.logging = Some(config);
354        self
355    }
356
357    pub fn security_config(mut self, config: SecurityConfig) -> Self {
358        self.config.security = Some(config);
359        self
360    }
361
362    pub fn error_pages_config(mut self, config: ErrorPagesConfig) -> Self {
363        self.config.error_pages = Some(config);
364        self
365    }
366
367    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
368        self.config.headers = Some(headers);
369        self
370    }
371
372    pub fn from_file<P: AsRef<std::path::Path>>(mut self, path: P) -> crate::Result<Self> {
373        let file_config = GurtConfig::from_file(path)?;
374        self.config = merge_configs(file_config, self.config);
375        Ok(self)
376    }
377
378    pub fn merge_cli_args(mut self, cli_args: &crate::cli::ServeCommand) -> Self {
379        self.config.server.host = cli_args.host.clone();
380        self.config.server.port = cli_args.port;
381        self.config.server.base_directory = Arc::new(cli_args.dir.clone());
382        self.config.server.verbose = cli_args.verbose;
383
384        if let (Some(cert), Some(key)) = (&cli_args.cert, &cli_args.key) {
385            self.config.tls = Some(TlsConfig {
386                certificate: cert.clone(),
387                private_key: key.clone(),
388            });
389        }
390
391        self
392    }
393
394    pub fn build(self) -> crate::Result<GurtConfig> {
395        let config = self.config;
396
397        if !config.server.base_directory.exists() || !config.server.base_directory.is_dir() {
398            return Err(crate::ServerError::InvalidConfiguration(
399                format!("Invalid base directory: {}", config.server.base_directory.display())
400            ));
401        }
402
403        if let Some(tls) = &config.tls {
404            if !tls.certificate.exists() {
405                return Err(crate::ServerError::TlsConfiguration(
406                    format!("Certificate file does not exist: {}", tls.certificate.display())
407                ));
408            }
409            if !tls.private_key.exists() {
410                return Err(crate::ServerError::TlsConfiguration(
411                    format!("Private key file does not exist: {}", tls.private_key.display())
412                ));
413            }
414        }
415
416        Ok(config)
417    }
418}
419
420
421fn parse_size(size_str: &str) -> crate::Result<u64> {
422    let size_str = size_str.trim().to_uppercase();
423    
424    if let Some(captures) = regex::Regex::new(r"^(\d+(?:\.\d+)?)\s*([KMGT]?B?)$").unwrap().captures(&size_str) {
425        let number: f64 = captures[1].parse()
426            .map_err(|_| crate::ServerError::InvalidConfiguration(format!("Invalid size format: {}", size_str)))?;
427        
428        let unit = captures.get(2).map_or("", |m| m.as_str());
429        
430        let multiplier: u64 = match unit {
431            "" | "B" => 1,
432            "KB" => 1_000,
433            "MB" => 1_000_000,
434            "GB" => 1_000_000_000,
435            "TB" => 1_000_000_000_000,
436            _ => return Err(crate::ServerError::InvalidConfiguration(format!("Unknown size unit: {}", unit))),
437        };
438        let number = (number * multiplier as f64) as u64;
439        Ok(number)
440    } else {
441        Err(crate::ServerError::InvalidConfiguration(format!("Invalid size format: {}", size_str)))
442    }
443}
444
445fn matches_pattern(path: &str, pattern: &str) -> bool {
446    if pattern.ends_with("/*") {
447        let prefix = &pattern[..pattern.len() - 2];
448        path.starts_with(prefix)
449    } else if pattern.starts_with("*.") {
450        let suffix = &pattern[1..];
451        path.ends_with(suffix)
452    } else {
453        path == pattern
454    }
455}
456
457fn merge_configs(base: GurtConfig, override_config: GurtConfig) -> GurtConfig {
458    GurtConfig {
459        server: merge_server_configs(base.server, override_config.server),
460        tls: override_config.tls.or(base.tls),
461        logging: override_config.logging.or(base.logging),
462        security: override_config.security.or(base.security),
463        error_pages: override_config.error_pages.or(base.error_pages),
464        headers: override_config.headers.or(base.headers),
465    }
466}
467
468fn merge_server_configs(base: ServerConfig, override_config: ServerConfig) -> ServerConfig {
469    ServerConfig {
470        host: if override_config.host != default_host() { override_config.host } else { base.host },
471        port: if override_config.port != default_port() { override_config.port } else { base.port },
472        protocol_version: if override_config.protocol_version != default_protocol_version() { 
473            override_config.protocol_version 
474        } else { 
475            base.protocol_version 
476        },
477        alpn_identifier: if override_config.alpn_identifier != default_alpn_identifier() { 
478            override_config.alpn_identifier 
479        } else { 
480            base.alpn_identifier 
481        },
482        timeouts: override_config.timeouts.or(base.timeouts),
483        max_connections: if override_config.max_connections != default_max_connections() { 
484            override_config.max_connections 
485        } else { 
486            base.max_connections 
487        },
488        max_message_size: if override_config.max_message_size != default_max_message_size() { 
489            override_config.max_message_size 
490        } else { 
491            base.max_message_size 
492        },
493        base_directory: override_config.base_directory,
494        verbose: override_config.verbose,
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    use std::path::PathBuf;
502
503    #[test]
504    fn test_default_config_creation() {
505        let base_dir = PathBuf::from("/tmp");
506        let mut config = GurtConfig::default();
507        config.server.base_directory = Arc::new(base_dir.clone());
508        
509        assert_eq!(config.server.host, "127.0.0.1");
510        assert_eq!(config.server.port, 4878);
511        assert_eq!(config.server.protocol_version, "1.0.0");
512        assert_eq!(config.server.alpn_identifier, "GURT/1.0");
513        assert_eq!(*config.server.base_directory, base_dir);
514    }
515
516    #[test]
517    fn test_config_from_valid_toml() {
518        let toml_content = r#"
519[server]
520host = "0.0.0.0"
521port = 8080
522protocol_version = "2.0.0"
523alpn_identifier = "custom"
524max_connections = 1000
525max_message_size = "10MB"
526
527[security]
528rate_limit_requests = 60
529rate_limit_connections = 5
530"#;
531        
532        let base_dir = PathBuf::from("/tmp");
533        let config = GurtConfig::from_toml(toml_content, base_dir).unwrap();
534        
535        assert_eq!(config.server.host, "0.0.0.0");
536        assert_eq!(config.server.port, 8080);
537        assert_eq!(config.server.protocol_version, "2.0.0");
538        assert_eq!(config.server.alpn_identifier, "custom");
539        assert_eq!(config.server.max_connections, 1000);
540        
541        let security = config.security.unwrap();
542        assert_eq!(security.rate_limit_requests, 60);
543        assert_eq!(security.rate_limit_connections, 5);
544    }
545
546    #[test]
547    fn test_invalid_toml_returns_error() {
548        let invalid_toml = r#"
549[server
550host = "0.0.0.0"
551"#;
552        
553        let base_dir = PathBuf::from("/tmp");
554        let result = GurtConfig::from_toml(invalid_toml, base_dir);
555        
556        assert!(result.is_err());
557    }
558
559    #[test]
560    fn test_max_message_size_parsing() {
561        let config = GurtConfig::default();
562        
563        assert_eq!(parse_size("1024").unwrap(), 1024);
564        assert_eq!(parse_size("1KB").unwrap(), 1000);
565        assert_eq!(parse_size("1MB").unwrap(), 1000 * 1000);
566        assert_eq!(parse_size("1GB").unwrap(), 1000 * 1000 * 1000);
567        
568        assert!(parse_size("invalid").is_err());
569        
570        assert!(config.max_message_size_bytes().is_ok());
571    }
572
573    #[test]
574    fn test_tls_config_validation() {
575        let mut config = GurtConfig::default();
576        
577        config.tls = Some(TlsConfig {
578            certificate: PathBuf::from("/nonexistent/cert.pem"),
579            private_key: PathBuf::from("/nonexistent/key.pem"),
580        });
581        
582        assert!(config.tls.is_some());
583        let tls = config.tls.unwrap();
584        assert_eq!(tls.certificate, PathBuf::from("/nonexistent/cert.pem"));
585        assert_eq!(tls.private_key, PathBuf::from("/nonexistent/key.pem"));
586    }
587
588    #[test]
589    fn test_address_formatting() {
590        let config = GurtConfig::default();
591        assert_eq!(config.address(), "127.0.0.1:4878");
592        
593        let mut custom_config = GurtConfig::default();
594        custom_config.server.host = "0.0.0.0".to_string();
595        custom_config.server.port = 8080;
596        assert_eq!(custom_config.address(), "0.0.0.0:8080");
597    }
598
599    #[test]
600    fn test_timeout_getters() {
601        let config = GurtConfig::default();
602        
603        assert_eq!(config.get_handshake_timeout(), Duration::from_secs(5));
604        assert_eq!(config.get_request_timeout(), Duration::from_secs(30));
605        assert_eq!(config.get_connection_timeout(), Duration::from_secs(10));
606    }
607}