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}