1use std::net::IpAddr;
10use std::path::Path;
11
12use serde::{Deserialize, Serialize};
13
14use crate::api::{CorsConfig, SecurityConfig, ServerConfig};
15use crate::cli::Args;
16use crate::security::{AuthConfig, CapabilitySet, RateLimitConfig};
17
18#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20#[serde(default)]
21pub struct Config {
22 pub server: ServerSection,
24 pub security: SecuritySection,
26 pub logging: LoggingSection,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(default)]
33pub struct ServerSection {
34 pub host: String,
36 pub port: u16,
38 pub graceful_shutdown: bool,
40}
41
42impl Default for ServerSection {
43 fn default() -> Self {
44 Self {
45 host: "127.0.0.1".to_string(),
46 port: 3000,
47 graceful_shutdown: true,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Default, Serialize, Deserialize)]
54#[serde(default)]
55pub struct SecuritySection {
56 pub auth: AuthSection,
58 pub rate_limit: RateLimitSection,
60 pub cors: CorsSection,
62}
63
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
66#[serde(default)]
67pub struct CorsSection {
68 pub allow_any: bool,
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75#[serde(default)]
76pub struct AuthSection {
77 pub enabled: bool,
79 pub api_keys: Vec<String>,
81 pub capabilities: Vec<String>,
83 pub preset: Option<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(default)]
90pub struct RateLimitSection {
91 pub enabled: bool,
93 pub requests_per_window: u32,
95 pub window_secs: u64,
97}
98
99impl Default for RateLimitSection {
100 fn default() -> Self {
101 Self {
102 enabled: true,
103 requests_per_window: 100,
104 window_secs: 60,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(default)]
112pub struct LoggingSection {
113 pub level: String,
115}
116
117impl Default for LoggingSection {
118 fn default() -> Self {
119 Self {
120 level: "info".to_string(),
121 }
122 }
123}
124
125impl Config {
126 pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
128 let content = std::fs::read_to_string(path).map_err(ConfigError::Io)?;
129 serde_json::from_str(&content).map_err(ConfigError::Json)
130 }
131
132 pub fn apply_env(&mut self) {
134 if let Ok(host) = std::env::var("SHELL_TUNNEL_HOST") {
135 self.server.host = host;
136 }
137
138 if let Ok(port) = std::env::var("SHELL_TUNNEL_PORT") {
139 if let Ok(port) = port.parse() {
140 self.server.port = port;
141 }
142 }
143
144 if let Ok(key) = std::env::var("SHELL_TUNNEL_API_KEY") {
145 if !key.is_empty() {
146 self.security.auth.enabled = true;
147 if !self.security.auth.api_keys.contains(&key) {
148 self.security.auth.api_keys.push(key);
149 }
150 }
151 }
152
153 if let Ok(level) = std::env::var("SHELL_TUNNEL_LOG_LEVEL") {
154 self.logging.level = level;
155 } else if let Ok(level) = std::env::var("RUST_LOG") {
156 self.logging.level = level;
157 }
158 }
159
160 pub fn apply_args(&mut self, args: &Args) {
162 self.server.host = args.host.to_string();
163 self.server.port = args.port;
164
165 if let Some(ref key) = args.api_key {
166 self.security.auth.enabled = true;
167 if !self.security.auth.api_keys.contains(key) {
168 self.security.auth.api_keys.push(key.clone());
169 }
170 }
171
172 if args.require_auth {
175 self.security.auth.enabled = true;
176 }
177
178 if !args.capabilities.is_empty() {
183 self.security.auth.capabilities = args.capabilities.clone();
184 self.security.auth.enabled = true;
185 }
186 if let Some(ref preset) = args.preset {
187 self.security.auth.preset = Some(preset.clone());
188 self.security.auth.enabled = true;
189 }
190
191 if args.no_auth {
192 self.security.auth.enabled = false;
193 }
194
195 if args.no_rate_limit {
196 self.security.rate_limit.enabled = false;
197 }
198
199 if args.cors_allow_any {
200 self.security.cors.allow_any = true;
201 }
202
203 if let Some(ref level) = args.log_level {
204 self.logging.level = level.clone();
205 }
206 }
207
208 pub fn load(args: &Args) -> Result<Self, ConfigError> {
212 let mut config = Config::default();
214
215 if let Some(ref path) = args.config {
217 config = Config::from_file(path)?;
218 }
219
220 config.apply_env();
222
223 config.apply_args(args);
225
226 Ok(config)
227 }
228
229 pub fn to_server_config(&self) -> Result<ServerConfig, ConfigError> {
231 let host: IpAddr = self
232 .server
233 .host
234 .parse()
235 .map_err(|_| ConfigError::InvalidHost(self.server.host.clone()))?;
236
237 let mut security = if self.security.auth.enabled {
238 SecurityConfig::secure()
239 } else {
240 SecurityConfig::development()
241 };
242
243 security.auth = AuthConfig {
245 enabled: self.security.auth.enabled,
246 ..AuthConfig::default()
247 };
248
249 security.rate_limit = RateLimitConfig {
251 enabled: self.security.rate_limit.enabled,
252 max_requests: self.security.rate_limit.requests_per_window,
253 window: std::time::Duration::from_secs(self.security.rate_limit.window_secs),
254 max_tracked_ips: 10000,
255 };
256
257 security.cors = CorsConfig {
259 allow_any: self.security.cors.allow_any,
260 };
261
262 if let Some(capabilities) = resolve_capabilities(
264 self.security.auth.preset.as_deref(),
265 &self.security.auth.capabilities,
266 )? {
267 security = security.with_capabilities(capabilities);
268 }
269
270 for key in &self.security.auth.api_keys {
272 security = security.with_api_key(key);
273 }
274
275 let mut server_config = ServerConfig::new(host.to_string(), self.server.port);
276 server_config = server_config.with_security(security);
277
278 if !self.server.graceful_shutdown {
279 server_config = server_config.without_graceful_shutdown();
280 }
281
282 Ok(server_config)
283 }
284
285 pub fn log_filter(&self) -> &str {
287 &self.logging.level
288 }
289}
290
291fn resolve_capabilities(
297 preset: Option<&str>,
298 capabilities: &[String],
299) -> Result<Option<CapabilitySet>, ConfigError> {
300 if preset.is_none() && capabilities.is_empty() {
301 return Ok(None); }
303
304 let mut set = match preset {
305 Some(name) => crate::security::preset(name)
306 .ok_or_else(|| ConfigError::InvalidPreset(name.to_string()))?,
307 None => CapabilitySet::new(),
308 };
309 for capability in capabilities {
310 set.insert(capability.clone());
311 }
312 Ok(Some(set))
313}
314
315#[derive(Debug)]
317pub enum ConfigError {
318 Io(std::io::Error),
320 Json(serde_json::Error),
322 InvalidHost(String),
324 InvalidPreset(String),
326}
327
328impl std::fmt::Display for ConfigError {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 match self {
331 Self::Io(e) => write!(f, "failed to read config file: {}", e),
332 Self::Json(e) => write!(f, "failed to parse config file: {}", e),
333 Self::InvalidHost(host) => write!(f, "invalid host address: {}", host),
334 Self::InvalidPreset(name) => write!(
335 f,
336 "unknown role preset: '{}' (expected operator, read-only, or full-control)",
337 name
338 ),
339 }
340 }
341}
342
343impl std::error::Error for ConfigError {}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use std::io::Write;
349 use tempfile::NamedTempFile;
350
351 #[test]
352 fn test_default_config() {
353 let config = Config::default();
354 assert_eq!(config.server.host, "127.0.0.1");
355 assert_eq!(config.server.port, 3000);
356 assert!(!config.security.auth.enabled);
357 assert!(config.security.rate_limit.enabled);
358 }
359
360 #[test]
361 fn test_config_from_json() {
362 let json = r#"{
363 "server": {
364 "host": "0.0.0.0",
365 "port": 8080
366 },
367 "security": {
368 "auth": {
369 "enabled": true,
370 "api_keys": ["key1", "key2"]
371 }
372 }
373 }"#;
374
375 let mut file = NamedTempFile::new().unwrap();
376 file.write_all(json.as_bytes()).unwrap();
377
378 let config = Config::from_file(file.path()).unwrap();
379 assert_eq!(config.server.host, "0.0.0.0");
380 assert_eq!(config.server.port, 8080);
381 assert!(config.security.auth.enabled);
382 assert_eq!(config.security.auth.api_keys.len(), 2);
383 }
384
385 #[test]
386 fn test_config_partial_json() {
387 let json = r#"{
388 "server": {
389 "port": 9000
390 }
391 }"#;
392
393 let mut file = NamedTempFile::new().unwrap();
394 file.write_all(json.as_bytes()).unwrap();
395
396 let config = Config::from_file(file.path()).unwrap();
397 assert_eq!(config.server.host, "127.0.0.1"); assert_eq!(config.server.port, 9000);
399 }
400
401 #[test]
402 fn test_apply_args() {
403 let mut config = Config::default();
404 let args = Args {
405 host: "192.168.1.1".parse().unwrap(),
406 port: 5000,
407 api_key: Some("test-key".to_string()),
408 no_rate_limit: true,
409 ..Args::default()
410 };
411
412 config.apply_args(&args);
413
414 assert_eq!(config.server.host, "192.168.1.1");
415 assert_eq!(config.server.port, 5000);
416 assert!(config.security.auth.enabled);
417 assert!(config
418 .security
419 .auth
420 .api_keys
421 .contains(&"test-key".to_string()));
422 assert!(!config.security.rate_limit.enabled);
423 }
424
425 #[test]
426 fn test_apply_no_auth() {
427 let mut config = Config::default();
428 config.security.auth.enabled = true;
429
430 let args = Args {
431 no_auth: true,
432 ..Args::default()
433 };
434
435 config.apply_args(&args);
436 assert!(!config.security.auth.enabled);
437 }
438
439 #[test]
440 fn test_apply_require_auth() {
441 let mut config = Config::default();
442 assert!(!config.security.auth.enabled); config.apply_args(&Args {
445 require_auth: true,
446 ..Args::default()
447 });
448 assert!(config.security.auth.enabled);
449 }
450
451 #[test]
452 fn test_no_auth_overrides_require_auth() {
453 let mut config = Config::default();
454
455 config.apply_args(&Args {
457 require_auth: true,
458 no_auth: true,
459 ..Args::default()
460 });
461 assert!(!config.security.auth.enabled);
462 }
463
464 #[test]
465 fn test_to_server_config() {
466 let config = Config::default();
467 let server_config = config.to_server_config().unwrap();
468
469 assert_eq!(server_config.host, "127.0.0.1");
470 assert_eq!(server_config.port, 3000);
471 }
472
473 #[test]
474 fn test_apply_args_capabilities_and_preset() {
475 let mut config = Config::default();
476 config.apply_args(&Args {
477 capabilities: vec!["exec".to_string(), "session.read".to_string()],
478 preset: Some("operator".to_string()),
479 ..Args::default()
480 });
481 assert_eq!(
482 config.security.auth.capabilities,
483 vec!["exec", "session.read"]
484 );
485 assert_eq!(config.security.auth.preset, Some("operator".to_string()));
486 }
487
488 #[test]
489 fn test_scope_implies_auth_on() {
490 let mut by_preset = Config::default();
493 by_preset.apply_args(&Args {
494 preset: Some("read-only".to_string()),
495 ..Args::default()
496 });
497 assert!(by_preset.security.auth.enabled);
498
499 let mut by_caps = Config::default();
500 by_caps.apply_args(&Args {
501 capabilities: vec!["session.read".to_string()],
502 ..Args::default()
503 });
504 assert!(by_caps.security.auth.enabled);
505 }
506
507 #[test]
508 fn test_no_auth_overrides_scope_implied_auth() {
509 let mut config = Config::default();
511 config.apply_args(&Args {
512 preset: Some("read-only".to_string()),
513 no_auth: true,
514 ..Args::default()
515 });
516 assert!(!config.security.auth.enabled);
517 }
518
519 #[test]
520 fn test_config_from_json_with_capabilities_and_preset() {
521 let json = r#"{
524 "security": {
525 "auth": {
526 "enabled": true,
527 "api_keys": ["scoped"],
528 "preset": "read-only",
529 "capabilities": ["exec"]
530 }
531 }
532 }"#;
533 let mut file = NamedTempFile::new().unwrap();
534 file.write_all(json.as_bytes()).unwrap();
535
536 let config = Config::from_file(file.path()).unwrap();
537 assert_eq!(config.security.auth.preset, Some("read-only".to_string()));
538 assert_eq!(config.security.auth.capabilities, vec!["exec"]);
539
540 let server_config = config.to_server_config().unwrap();
541 let caps = server_config
542 .security
543 .capabilities
544 .expect("capabilities scoped from file");
545 assert!(caps.satisfies("session.read")); assert!(caps.satisfies("exec")); assert!(!caps.satisfies("session.manage"));
548 }
549
550 #[test]
551 fn test_resolve_capabilities_none_by_default() {
552 assert!(resolve_capabilities(None, &[]).unwrap().is_none());
554 }
555
556 #[test]
557 fn test_resolve_capabilities_preset_plus_extra() {
558 let set = resolve_capabilities(Some("read-only"), &["exec".to_string()])
560 .unwrap()
561 .unwrap();
562 assert!(set.satisfies("session.read"));
563 assert!(set.satisfies("exec"));
564 assert!(!set.satisfies("session.manage"));
565 }
566
567 #[test]
568 fn test_resolve_capabilities_invalid_preset_errors() {
569 let err = resolve_capabilities(Some("superuser"), &[]);
570 assert!(matches!(err, Err(ConfigError::InvalidPreset(_))));
571 }
572
573 #[test]
574 fn test_to_server_config_scopes_capabilities() {
575 let mut config = Config::default();
576 config.security.auth.enabled = true;
577 config.security.auth.api_keys = vec!["scoped".to_string()];
578 config.security.auth.preset = Some("read-only".to_string());
579
580 let server_config = config.to_server_config().unwrap();
581 let caps = server_config
582 .security
583 .capabilities
584 .expect("capabilities scoped");
585 assert!(caps.satisfies("session.read"));
586 assert!(!caps.satisfies("exec"));
587 }
588
589 #[test]
590 fn test_to_server_config_invalid_preset_errors() {
591 let mut config = Config::default();
592 config.security.auth.preset = Some("root".to_string());
593 assert!(matches!(
594 config.to_server_config(),
595 Err(ConfigError::InvalidPreset(_))
596 ));
597 }
598
599 #[test]
600 fn test_invalid_host() {
601 let mut config = Config::default();
602 config.server.host = "not-an-ip".to_string();
603
604 let result = config.to_server_config();
605 assert!(result.is_err());
606 }
607
608 #[test]
609 fn test_config_serialization() {
610 let config = Config::default();
611 let json = serde_json::to_string_pretty(&config).unwrap();
612 assert!(json.contains("\"host\""));
613 assert!(json.contains("\"port\""));
614 }
615}