1use std::net::SocketAddr;
4use std::sync::Arc;
5
6use axum::{
7 extract::{connect_info::IntoMakeServiceWithConnectInfo, MatchedPath, Request, State},
8 http::{header::AUTHORIZATION, Method, StatusCode},
9 middleware::{self, Next},
10 response::Response,
11 routing::{any, get, post},
12 Router,
13};
14use tower_http::{
15 cors::{Any, CorsLayer},
16 trace::TraceLayer,
17};
18
19use super::handlers::{
20 api_info, create_session, delete_session, execute_command, execute_oneshot, get_session,
21 health, list_sessions, AppState,
22};
23use super::websocket::{ws_handler, ws_oneshot_handler};
24use crate::security::{
25 rate_limit_middleware, ApiKeyStore, AuthConfig, CapabilitySet, RateLimitConfig, RateLimiter,
26};
27
28#[derive(Debug, Clone, Default)]
40pub struct CorsConfig {
41 pub allow_any: bool,
44}
45
46#[derive(Debug, Clone)]
48pub struct SecurityConfig {
49 pub auth: AuthConfig,
51 pub rate_limit: RateLimitConfig,
53 pub api_keys: Vec<String>,
55 pub capabilities: Option<CapabilitySet>,
62 pub cors: CorsConfig,
64 pub allowed_hosts: Option<Vec<String>>,
73}
74
75impl Default for SecurityConfig {
76 fn default() -> Self {
77 Self {
78 auth: AuthConfig::disabled(), rate_limit: RateLimitConfig::default(),
80 api_keys: Vec::new(),
81 capabilities: None, cors: CorsConfig::default(), allowed_hosts: None,
84 }
85 }
86}
87
88fn cors_layer(cfg: &CorsConfig) -> Option<CorsLayer> {
92 cfg.allow_any.then(|| {
93 CorsLayer::new()
94 .allow_origin(Any)
95 .allow_methods(Any)
96 .allow_headers(Any)
97 })
98}
99
100impl SecurityConfig {
101 pub fn secure() -> Self {
103 Self {
104 auth: AuthConfig::default(),
105 rate_limit: RateLimitConfig::default(),
106 api_keys: Vec::new(),
107 capabilities: None,
108 cors: CorsConfig::default(),
109 allowed_hosts: None,
110 }
111 }
112
113 pub fn development() -> Self {
115 Self {
116 auth: AuthConfig::disabled(),
117 rate_limit: RateLimitConfig::relaxed(),
118 api_keys: Vec::new(),
119 capabilities: None,
120 cors: CorsConfig::default(),
121 allowed_hosts: None,
122 }
123 }
124
125 pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
127 self.api_keys.push(key.into());
128 self
129 }
130
131 pub fn with_capabilities(mut self, capabilities: CapabilitySet) -> Self {
136 self.capabilities = Some(capabilities);
137 self
138 }
139
140 pub fn with_allowed_hosts(mut self, hosts: Vec<String>) -> Self {
142 self.allowed_hosts = Some(hosts);
143 self
144 }
145
146 pub fn with_cors_allow_any(mut self) -> Self {
148 self.cors.allow_any = true;
149 self
150 }
151}
152
153fn register_key(store: &ApiKeyStore, key: &str, capabilities: &Option<CapabilitySet>) {
156 match capabilities {
157 Some(caps) => store.add_key_with_capabilities(key, caps.clone(), "configured"),
158 None => store.add_key(key),
159 }
160}
161
162pub fn create_router() -> Router {
164 create_router_with_state(AppState::new())
165}
166
167pub fn create_router_with_state(state: AppState) -> Router {
169 let session_routes = Router::new()
171 .route("/", get(list_sessions).post(create_session))
172 .route("/{id}", get(get_session).delete(delete_session))
173 .route("/{id}/execute", post(execute_command))
174 .route("/{id}/ws", any(ws_handler));
175
176 let api_v1 = Router::new()
178 .route("/", get(api_info))
179 .route("/execute", post(execute_oneshot))
180 .route("/ws", any(ws_oneshot_handler))
181 .nest("/sessions", session_routes);
182
183 Router::new()
186 .route("/health", get(health))
187 .nest("/api/v1", api_v1)
188 .layer(TraceLayer::new_for_http())
189 .with_state(state)
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum RequiredCapability {
199 Public,
201 Authenticated,
204 Capability(&'static str),
206}
207
208pub fn required_capability(method: &Method, matched_path: &str) -> RequiredCapability {
218 use RequiredCapability::{Authenticated, Capability, Public};
219
220 match (method, matched_path) {
221 (_, "/health") => Public,
222 (&Method::GET, "/api/v1") => Authenticated,
223 (&Method::POST, "/api/v1/execute") => Capability("exec"),
224 (_, "/api/v1/ws") => Capability("exec"),
225 (&Method::GET, "/api/v1/sessions") => Capability("session.read"),
226 (&Method::POST, "/api/v1/sessions") => Capability("session.manage"),
227 (&Method::GET, "/api/v1/sessions/{id}") => Capability("session.read"),
228 (&Method::DELETE, "/api/v1/sessions/{id}") => Capability("session.manage"),
229 (&Method::POST, "/api/v1/sessions/{id}/execute") => Capability("exec"),
230 (_, "/api/v1/sessions/{id}/ws") => Capability("exec"),
231 _ => Authenticated,
232 }
233}
234
235async fn capability_auth_middleware(
244 State((store, audit)): State<(
245 std::sync::Arc<ApiKeyStore>,
246 std::sync::Arc<crate::audit::AuditSink>,
247 )>,
248 mut request: Request,
249 next: Next,
250) -> Result<Response, StatusCode> {
251 if !store.is_enabled() {
253 return Ok(next.run(request).await);
254 }
255
256 let method = request.method().clone();
257 let matched = request
259 .extensions()
260 .get::<MatchedPath>()
261 .map(|m| m.as_str().to_owned())
262 .unwrap_or_default();
263 let required = required_capability(&method, &matched);
264
265 if required == RequiredCapability::Public {
267 return Ok(next.run(request).await);
268 }
269
270 let token = request
273 .headers()
274 .get(AUTHORIZATION)
275 .and_then(|v| v.to_str().ok())
276 .and_then(|header| store.extract_key(header));
277
278 let identity = token.as_deref().and_then(|t| store.identity(t));
279
280 let capabilities = match token.as_deref().and_then(|t| store.capabilities(t)) {
281 Some(caps) => caps,
282 None => {
283 let reason = if token.is_none() {
287 "missing-token"
288 } else {
289 "invalid-token"
290 };
291 tracing::debug!(%method, path = %matched, reason, "auth rejected (401)");
292 audit.record(
295 crate::audit::AuditEvent::new("denied")
296 .with_route(format!("{method} {matched}"))
297 .with_denial(401, reason),
298 );
299 return Err(StatusCode::UNAUTHORIZED);
300 }
301 };
302
303 if let Some(identity) = identity.clone() {
305 request.extensions_mut().insert(identity);
306 }
307
308 match required {
309 RequiredCapability::Public => Ok(next.run(request).await),
311 RequiredCapability::Authenticated => Ok(next.run(request).await),
313 RequiredCapability::Capability(cap) => {
315 if capabilities.satisfies(cap) {
316 Ok(next.run(request).await)
317 } else {
318 audit.record(
319 crate::audit::AuditEvent::new("denied")
320 .with_identity(identity)
321 .with_route(format!("{method} {matched}"))
322 .with_denial(403, format!("missing-capability:{cap}")),
323 );
324 tracing::debug!(
325 %method,
326 path = %matched,
327 required = cap,
328 "authorization denied (403): insufficient capability"
329 );
330 Err(StatusCode::FORBIDDEN)
331 }
332 }
333 }
334}
335
336fn host_is_allowed(header: Option<&str>, allowed: &[String]) -> bool {
342 let Some(value) = header else {
343 return false;
346 };
347
348 let host = value
349 .rsplit_once(':')
350 .map_or(value, |(host, port)| {
351 if port.chars().all(|c| c.is_ascii_digit()) {
353 host
354 } else {
355 value
356 }
357 })
358 .trim_matches(|c| c == '[' || c == ']');
359
360 allowed
361 .iter()
362 .any(|candidate| candidate.eq_ignore_ascii_case(host))
363}
364
365async fn host_check_middleware(
367 State(allowed): State<Arc<Vec<String>>>,
368 request: Request,
369 next: Next,
370) -> Result<Response, (StatusCode, String)> {
371 let header = request
372 .headers()
373 .get(axum::http::header::HOST)
374 .and_then(|value| value.to_str().ok());
375
376 if host_is_allowed(header, &allowed) {
377 return Ok(next.run(request).await);
378 }
379
380 let seen = header.unwrap_or("(none)").to_string();
383 tracing::debug!(host = %seen, "request refused: host not allowed");
384 Err((
385 StatusCode::FORBIDDEN,
386 format!(
387 "host {seen} is not allowed; pass --allow-host {seen} to permit it
388"
389 ),
390 ))
391}
392
393pub fn create_secure_router(
395 state: AppState,
396 security: SecurityConfig,
397) -> (Router, Arc<ApiKeyStore>, Arc<RateLimiter>) {
398 let auth_store = Arc::new(ApiKeyStore::new(security.auth));
400 let rate_limiter = Arc::new(RateLimiter::new(security.rate_limit));
401
402 for key in &security.api_keys {
404 register_key(&auth_store, key, &security.capabilities);
405 }
406
407 let session_routes = Router::new()
409 .route("/", get(list_sessions).post(create_session))
410 .route("/{id}", get(get_session).delete(delete_session))
411 .route("/{id}/execute", post(execute_command))
412 .route("/{id}/ws", any(ws_handler));
413
414 let api_v1 = Router::new()
416 .route("/", get(api_info))
417 .route("/execute", post(execute_oneshot))
418 .route("/ws", any(ws_oneshot_handler))
419 .nest("/sessions", session_routes);
420
421 let allowed_hosts = security.allowed_hosts.clone();
422
423 let mut router = Router::new()
425 .route("/health", get(health))
426 .nest("/api/v1", api_v1)
427 .layer(middleware::from_fn_with_state(
428 (Arc::clone(&auth_store), Arc::clone(&state.audit)),
429 capability_auth_middleware,
430 ))
431 .layer(middleware::from_fn_with_state(
432 Arc::clone(&rate_limiter),
433 rate_limit_middleware,
434 ))
435 .layer(TraceLayer::new_for_http());
436
437 if let Some(hosts) = allowed_hosts {
440 router = router.layer(middleware::from_fn_with_state(
441 Arc::new(hosts),
442 host_check_middleware,
443 ));
444 }
445
446 if let Some(cors) = cors_layer(&security.cors) {
448 router = router.layer(cors);
449 }
450
451 let router = router.with_state(state);
452
453 (router, auth_store, rate_limiter)
454}
455
456#[derive(Debug, Clone)]
458pub struct ServerConfig {
459 pub host: String,
461 pub port: u16,
463 pub security: SecurityConfig,
465 pub graceful_shutdown: bool,
467}
468
469impl ServerConfig {
470 pub fn new(host: impl Into<String>, port: u16) -> Self {
471 Self {
472 host: host.into(),
473 port,
474 security: SecurityConfig::default(),
475 graceful_shutdown: true,
476 }
477 }
478
479 pub fn bind_address(&self) -> String {
480 format!("{}:{}", self.host, self.port)
481 }
482
483 pub fn with_security(mut self, security: SecurityConfig) -> Self {
485 self.security = security;
486 self
487 }
488
489 pub fn without_graceful_shutdown(mut self) -> Self {
491 self.graceful_shutdown = false;
492 self
493 }
494}
495
496impl Default for ServerConfig {
497 fn default() -> Self {
498 Self {
499 host: "127.0.0.1".to_string(),
500 port: 3000,
501 security: SecurityConfig::default(),
502 graceful_shutdown: true,
503 }
504 }
505}
506
507pub async fn serve(config: ServerConfig) -> crate::Result<()> {
509 serve_with_state(config, AppState::new()).await
510}
511
512pub async fn bind(config: &ServerConfig) -> crate::Result<tokio::net::TcpListener> {
520 tokio::net::TcpListener::bind(config.bind_address())
521 .await
522 .map_err(crate::error::ShellTunnelError::Io)
523}
524
525pub async fn serve_with_state(config: ServerConfig, state: AppState) -> crate::Result<()> {
527 let listener = bind(&config).await?;
528 serve_on(listener, config, state).await
529}
530
531pub async fn serve_on(
533 listener: tokio::net::TcpListener,
534 config: ServerConfig,
535 state: AppState,
536) -> crate::Result<()> {
537 let addr = config.bind_address();
538
539 let (router, auth_store, _rate_limiter) = create_secure_router(state, config.security.clone());
541
542 if auth_store.is_enabled() {
544 if auth_store.count() == 0 {
545 let key = crate::security::generate_api_key();
548 register_key(&auth_store, &key, &config.security.capabilities);
549 tracing::info!("Generated API key: {}", key);
550 }
551 tracing::info!(
552 "Authentication enabled with {} API key(s)",
553 auth_store.count()
554 );
555 } else {
556 tracing::warn!("Authentication is DISABLED - server is open to all requests");
557 }
558
559 let _ = addr;
560 tracing::info!(
561 "Starting shell-tunnel API server on {}",
562 listener
563 .local_addr()
564 .map(|a| a.to_string())
565 .unwrap_or_else(|_| config.bind_address())
566 );
567
568 let service: IntoMakeServiceWithConnectInfo<Router, SocketAddr> =
570 router.into_make_service_with_connect_info::<SocketAddr>();
571
572 if config.graceful_shutdown {
573 axum::serve(listener, service)
575 .with_graceful_shutdown(shutdown_signal())
576 .await
577 .map_err(|e| {
578 crate::error::ShellTunnelError::Io(std::io::Error::other(e.to_string()))
579 })?;
580
581 tracing::info!("Server shutdown complete");
582 } else {
583 axum::serve(listener, service).await.map_err(|e| {
585 crate::error::ShellTunnelError::Io(std::io::Error::other(e.to_string()))
586 })?;
587 }
588
589 Ok(())
590}
591
592async fn shutdown_signal() {
594 let ctrl_c = async {
595 tokio::signal::ctrl_c()
596 .await
597 .expect("Failed to install Ctrl+C handler");
598 };
599
600 #[cfg(unix)]
601 let terminate = async {
602 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
603 .expect("Failed to install SIGTERM handler")
604 .recv()
605 .await;
606 };
607
608 #[cfg(not(unix))]
609 let terminate = std::future::pending::<()>();
610
611 tokio::select! {
612 _ = ctrl_c => {
613 tracing::info!("Received Ctrl+C, initiating graceful shutdown...");
614 }
615 _ = terminate => {
616 tracing::info!("Received SIGTERM, initiating graceful shutdown...");
617 }
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn test_server_config_default() {
627 let config = ServerConfig::default();
628 assert_eq!(config.host, "127.0.0.1");
629 assert_eq!(config.port, 3000);
630 assert_eq!(config.bind_address(), "127.0.0.1:3000");
631 assert!(config.graceful_shutdown);
632 }
633
634 #[test]
635 fn test_server_config_custom() {
636 let config = ServerConfig::new("0.0.0.0", 8080);
637 assert_eq!(config.bind_address(), "0.0.0.0:8080");
638 }
639
640 #[test]
641 fn test_server_config_with_security() {
642 let config = ServerConfig::new("0.0.0.0", 8080)
643 .with_security(SecurityConfig::secure().with_api_key("test-key"));
644
645 assert!(config.security.auth.enabled);
646 assert_eq!(config.security.api_keys.len(), 1);
647 }
648
649 #[test]
650 fn test_security_config_default() {
651 let config = SecurityConfig::default();
652 assert!(!config.auth.enabled); assert!(config.rate_limit.enabled);
654 }
655
656 #[test]
657 fn test_security_config_secure() {
658 let config = SecurityConfig::secure();
659 assert!(config.auth.enabled);
660 assert!(config.rate_limit.enabled);
661 }
662
663 #[test]
664 fn test_cors_restrictive_by_default() {
665 assert!(!SecurityConfig::default().cors.allow_any);
666 assert!(!SecurityConfig::secure().cors.allow_any);
667 assert!(cors_layer(&CorsConfig::default()).is_none());
668 }
669
670 #[test]
671 fn test_cors_allow_any_opt_in() {
672 let config = SecurityConfig::development().with_cors_allow_any();
673 assert!(config.cors.allow_any);
674 assert!(cors_layer(&config.cors).is_some());
675 }
676
677 #[test]
678 fn test_security_config_development() {
679 let config = SecurityConfig::development();
680 assert!(!config.auth.enabled);
681 assert!(config.rate_limit.enabled);
682 }
683
684 #[test]
685 fn test_router_creation() {
686 let _router = create_router();
687 }
689
690 #[test]
691 fn test_required_capability_mapping() {
692 use RequiredCapability::{Authenticated, Capability, Public};
693
694 assert_eq!(required_capability(&Method::GET, "/health"), Public);
696 assert_eq!(required_capability(&Method::GET, "/api/v1"), Authenticated);
697
698 assert_eq!(
700 required_capability(&Method::POST, "/api/v1/execute"),
701 Capability("exec")
702 );
703 assert_eq!(
704 required_capability(&Method::GET, "/api/v1/ws"),
705 Capability("exec")
706 );
707 assert_eq!(
708 required_capability(&Method::POST, "/api/v1/sessions/{id}/execute"),
709 Capability("exec")
710 );
711 assert_eq!(
712 required_capability(&Method::GET, "/api/v1/sessions/{id}/ws"),
713 Capability("exec")
714 );
715
716 assert_eq!(
718 required_capability(&Method::GET, "/api/v1/sessions"),
719 Capability("session.read")
720 );
721 assert_eq!(
722 required_capability(&Method::POST, "/api/v1/sessions"),
723 Capability("session.manage")
724 );
725 assert_eq!(
726 required_capability(&Method::GET, "/api/v1/sessions/{id}"),
727 Capability("session.read")
728 );
729 assert_eq!(
730 required_capability(&Method::DELETE, "/api/v1/sessions/{id}"),
731 Capability("session.manage")
732 );
733 }
734
735 #[test]
736 fn test_required_capability_unknown_fails_closed() {
737 assert_eq!(
739 required_capability(&Method::GET, "/api/v1/unknown"),
740 RequiredCapability::Authenticated
741 );
742 }
743
744 #[test]
745 fn test_secure_router_creation() {
746 let state = AppState::new();
747 let security = SecurityConfig::secure().with_api_key("test-key");
748 let (router, auth_store, rate_limiter) = create_secure_router(state, security);
749
750 assert_eq!(auth_store.count(), 1);
751 assert!(auth_store.is_valid("test-key"));
752 assert!(rate_limiter.is_enabled());
753
754 drop(router);
756 }
757}