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}
65
66impl Default for SecurityConfig {
67 fn default() -> Self {
68 Self {
69 auth: AuthConfig::disabled(), rate_limit: RateLimitConfig::default(),
71 api_keys: Vec::new(),
72 capabilities: None, cors: CorsConfig::default(), }
75 }
76}
77
78fn cors_layer(cfg: &CorsConfig) -> Option<CorsLayer> {
82 cfg.allow_any.then(|| {
83 CorsLayer::new()
84 .allow_origin(Any)
85 .allow_methods(Any)
86 .allow_headers(Any)
87 })
88}
89
90impl SecurityConfig {
91 pub fn secure() -> Self {
93 Self {
94 auth: AuthConfig::default(),
95 rate_limit: RateLimitConfig::default(),
96 api_keys: Vec::new(),
97 capabilities: None,
98 cors: CorsConfig::default(),
99 }
100 }
101
102 pub fn development() -> Self {
104 Self {
105 auth: AuthConfig::disabled(),
106 rate_limit: RateLimitConfig::relaxed(),
107 api_keys: Vec::new(),
108 capabilities: None,
109 cors: CorsConfig::default(),
110 }
111 }
112
113 pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
115 self.api_keys.push(key.into());
116 self
117 }
118
119 pub fn with_capabilities(mut self, capabilities: CapabilitySet) -> Self {
124 self.capabilities = Some(capabilities);
125 self
126 }
127
128 pub fn with_cors_allow_any(mut self) -> Self {
130 self.cors.allow_any = true;
131 self
132 }
133}
134
135fn register_key(store: &ApiKeyStore, key: &str, capabilities: &Option<CapabilitySet>) {
138 match capabilities {
139 Some(caps) => store.add_key_with_capabilities(key, caps.clone(), "configured"),
140 None => store.add_key(key),
141 }
142}
143
144pub fn create_router() -> Router {
146 create_router_with_state(AppState::new())
147}
148
149pub fn create_router_with_state(state: AppState) -> Router {
151 let session_routes = Router::new()
153 .route("/", get(list_sessions).post(create_session))
154 .route("/{id}", get(get_session).delete(delete_session))
155 .route("/{id}/execute", post(execute_command))
156 .route("/{id}/ws", any(ws_handler));
157
158 let api_v1 = Router::new()
160 .route("/", get(api_info))
161 .route("/execute", post(execute_oneshot))
162 .route("/ws", any(ws_oneshot_handler))
163 .nest("/sessions", session_routes);
164
165 Router::new()
168 .route("/health", get(health))
169 .nest("/api/v1", api_v1)
170 .layer(TraceLayer::new_for_http())
171 .with_state(state)
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum RequiredCapability {
181 Public,
183 Authenticated,
186 Capability(&'static str),
188}
189
190pub fn required_capability(method: &Method, matched_path: &str) -> RequiredCapability {
200 use RequiredCapability::{Authenticated, Capability, Public};
201
202 match (method, matched_path) {
203 (_, "/health") => Public,
204 (&Method::GET, "/api/v1") => Authenticated,
205 (&Method::POST, "/api/v1/execute") => Capability("exec"),
206 (_, "/api/v1/ws") => Capability("exec"),
207 (&Method::GET, "/api/v1/sessions") => Capability("session.read"),
208 (&Method::POST, "/api/v1/sessions") => Capability("session.manage"),
209 (&Method::GET, "/api/v1/sessions/{id}") => Capability("session.read"),
210 (&Method::DELETE, "/api/v1/sessions/{id}") => Capability("session.manage"),
211 (&Method::POST, "/api/v1/sessions/{id}/execute") => Capability("exec"),
212 (_, "/api/v1/sessions/{id}/ws") => Capability("exec"),
213 _ => Authenticated,
214 }
215}
216
217async fn capability_auth_middleware(
226 State(store): State<std::sync::Arc<ApiKeyStore>>,
227 request: Request,
228 next: Next,
229) -> Result<Response, StatusCode> {
230 if !store.is_enabled() {
232 return Ok(next.run(request).await);
233 }
234
235 let method = request.method().clone();
236 let matched = request
238 .extensions()
239 .get::<MatchedPath>()
240 .map(|m| m.as_str().to_owned())
241 .unwrap_or_default();
242 let required = required_capability(&method, &matched);
243
244 if required == RequiredCapability::Public {
246 return Ok(next.run(request).await);
247 }
248
249 let token = request
252 .headers()
253 .get(AUTHORIZATION)
254 .and_then(|v| v.to_str().ok())
255 .and_then(|header| store.extract_key(header));
256
257 let capabilities = match token.as_deref().and_then(|t| store.capabilities(t)) {
258 Some(caps) => caps,
259 None => {
260 let reason = if token.is_none() {
264 "missing-token"
265 } else {
266 "invalid-token"
267 };
268 tracing::debug!(%method, path = %matched, reason, "auth rejected (401)");
269 return Err(StatusCode::UNAUTHORIZED);
270 }
271 };
272
273 match required {
274 RequiredCapability::Public => Ok(next.run(request).await),
276 RequiredCapability::Authenticated => Ok(next.run(request).await),
278 RequiredCapability::Capability(cap) => {
280 if capabilities.satisfies(cap) {
281 Ok(next.run(request).await)
282 } else {
283 tracing::debug!(
284 %method,
285 path = %matched,
286 required = cap,
287 "authorization denied (403): insufficient capability"
288 );
289 Err(StatusCode::FORBIDDEN)
290 }
291 }
292 }
293}
294
295pub fn create_secure_router(
297 state: AppState,
298 security: SecurityConfig,
299) -> (Router, Arc<ApiKeyStore>, Arc<RateLimiter>) {
300 let auth_store = Arc::new(ApiKeyStore::new(security.auth));
302 let rate_limiter = Arc::new(RateLimiter::new(security.rate_limit));
303
304 for key in &security.api_keys {
306 register_key(&auth_store, key, &security.capabilities);
307 }
308
309 let session_routes = Router::new()
311 .route("/", get(list_sessions).post(create_session))
312 .route("/{id}", get(get_session).delete(delete_session))
313 .route("/{id}/execute", post(execute_command))
314 .route("/{id}/ws", any(ws_handler));
315
316 let api_v1 = Router::new()
318 .route("/", get(api_info))
319 .route("/execute", post(execute_oneshot))
320 .route("/ws", any(ws_oneshot_handler))
321 .nest("/sessions", session_routes);
322
323 let mut router = Router::new()
325 .route("/health", get(health))
326 .nest("/api/v1", api_v1)
327 .layer(middleware::from_fn_with_state(
328 Arc::clone(&auth_store),
329 capability_auth_middleware,
330 ))
331 .layer(middleware::from_fn_with_state(
332 Arc::clone(&rate_limiter),
333 rate_limit_middleware,
334 ))
335 .layer(TraceLayer::new_for_http());
336
337 if let Some(cors) = cors_layer(&security.cors) {
339 router = router.layer(cors);
340 }
341
342 let router = router.with_state(state);
343
344 (router, auth_store, rate_limiter)
345}
346
347#[derive(Debug, Clone)]
349pub struct ServerConfig {
350 pub host: String,
352 pub port: u16,
354 pub security: SecurityConfig,
356 pub graceful_shutdown: bool,
358}
359
360impl ServerConfig {
361 pub fn new(host: impl Into<String>, port: u16) -> Self {
362 Self {
363 host: host.into(),
364 port,
365 security: SecurityConfig::default(),
366 graceful_shutdown: true,
367 }
368 }
369
370 pub fn bind_address(&self) -> String {
371 format!("{}:{}", self.host, self.port)
372 }
373
374 pub fn with_security(mut self, security: SecurityConfig) -> Self {
376 self.security = security;
377 self
378 }
379
380 pub fn without_graceful_shutdown(mut self) -> Self {
382 self.graceful_shutdown = false;
383 self
384 }
385}
386
387impl Default for ServerConfig {
388 fn default() -> Self {
389 Self {
390 host: "127.0.0.1".to_string(),
391 port: 3000,
392 security: SecurityConfig::default(),
393 graceful_shutdown: true,
394 }
395 }
396}
397
398pub async fn serve(config: ServerConfig) -> crate::Result<()> {
400 serve_with_state(config, AppState::new()).await
401}
402
403pub async fn bind(config: &ServerConfig) -> crate::Result<tokio::net::TcpListener> {
411 tokio::net::TcpListener::bind(config.bind_address())
412 .await
413 .map_err(crate::error::ShellTunnelError::Io)
414}
415
416pub async fn serve_with_state(config: ServerConfig, state: AppState) -> crate::Result<()> {
418 let listener = bind(&config).await?;
419 serve_on(listener, config, state).await
420}
421
422pub async fn serve_on(
424 listener: tokio::net::TcpListener,
425 config: ServerConfig,
426 state: AppState,
427) -> crate::Result<()> {
428 let addr = config.bind_address();
429
430 let (router, auth_store, _rate_limiter) = create_secure_router(state, config.security.clone());
432
433 if auth_store.is_enabled() {
435 if auth_store.count() == 0 {
436 let key = crate::security::generate_api_key();
439 register_key(&auth_store, &key, &config.security.capabilities);
440 tracing::info!("Generated API key: {}", key);
441 }
442 tracing::info!(
443 "Authentication enabled with {} API key(s)",
444 auth_store.count()
445 );
446 } else {
447 tracing::warn!("Authentication is DISABLED - server is open to all requests");
448 }
449
450 let _ = addr;
451 tracing::info!(
452 "Starting shell-tunnel API server on {}",
453 listener
454 .local_addr()
455 .map(|a| a.to_string())
456 .unwrap_or_else(|_| config.bind_address())
457 );
458
459 let service: IntoMakeServiceWithConnectInfo<Router, SocketAddr> =
461 router.into_make_service_with_connect_info::<SocketAddr>();
462
463 if config.graceful_shutdown {
464 axum::serve(listener, service)
466 .with_graceful_shutdown(shutdown_signal())
467 .await
468 .map_err(|e| {
469 crate::error::ShellTunnelError::Io(std::io::Error::other(e.to_string()))
470 })?;
471
472 tracing::info!("Server shutdown complete");
473 } else {
474 axum::serve(listener, service).await.map_err(|e| {
476 crate::error::ShellTunnelError::Io(std::io::Error::other(e.to_string()))
477 })?;
478 }
479
480 Ok(())
481}
482
483async fn shutdown_signal() {
485 let ctrl_c = async {
486 tokio::signal::ctrl_c()
487 .await
488 .expect("Failed to install Ctrl+C handler");
489 };
490
491 #[cfg(unix)]
492 let terminate = async {
493 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
494 .expect("Failed to install SIGTERM handler")
495 .recv()
496 .await;
497 };
498
499 #[cfg(not(unix))]
500 let terminate = std::future::pending::<()>();
501
502 tokio::select! {
503 _ = ctrl_c => {
504 tracing::info!("Received Ctrl+C, initiating graceful shutdown...");
505 }
506 _ = terminate => {
507 tracing::info!("Received SIGTERM, initiating graceful shutdown...");
508 }
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn test_server_config_default() {
518 let config = ServerConfig::default();
519 assert_eq!(config.host, "127.0.0.1");
520 assert_eq!(config.port, 3000);
521 assert_eq!(config.bind_address(), "127.0.0.1:3000");
522 assert!(config.graceful_shutdown);
523 }
524
525 #[test]
526 fn test_server_config_custom() {
527 let config = ServerConfig::new("0.0.0.0", 8080);
528 assert_eq!(config.bind_address(), "0.0.0.0:8080");
529 }
530
531 #[test]
532 fn test_server_config_with_security() {
533 let config = ServerConfig::new("0.0.0.0", 8080)
534 .with_security(SecurityConfig::secure().with_api_key("test-key"));
535
536 assert!(config.security.auth.enabled);
537 assert_eq!(config.security.api_keys.len(), 1);
538 }
539
540 #[test]
541 fn test_security_config_default() {
542 let config = SecurityConfig::default();
543 assert!(!config.auth.enabled); assert!(config.rate_limit.enabled);
545 }
546
547 #[test]
548 fn test_security_config_secure() {
549 let config = SecurityConfig::secure();
550 assert!(config.auth.enabled);
551 assert!(config.rate_limit.enabled);
552 }
553
554 #[test]
555 fn test_cors_restrictive_by_default() {
556 assert!(!SecurityConfig::default().cors.allow_any);
557 assert!(!SecurityConfig::secure().cors.allow_any);
558 assert!(cors_layer(&CorsConfig::default()).is_none());
559 }
560
561 #[test]
562 fn test_cors_allow_any_opt_in() {
563 let config = SecurityConfig::development().with_cors_allow_any();
564 assert!(config.cors.allow_any);
565 assert!(cors_layer(&config.cors).is_some());
566 }
567
568 #[test]
569 fn test_security_config_development() {
570 let config = SecurityConfig::development();
571 assert!(!config.auth.enabled);
572 assert!(config.rate_limit.enabled);
573 }
574
575 #[test]
576 fn test_router_creation() {
577 let _router = create_router();
578 }
580
581 #[test]
582 fn test_required_capability_mapping() {
583 use RequiredCapability::{Authenticated, Capability, Public};
584
585 assert_eq!(required_capability(&Method::GET, "/health"), Public);
587 assert_eq!(required_capability(&Method::GET, "/api/v1"), Authenticated);
588
589 assert_eq!(
591 required_capability(&Method::POST, "/api/v1/execute"),
592 Capability("exec")
593 );
594 assert_eq!(
595 required_capability(&Method::GET, "/api/v1/ws"),
596 Capability("exec")
597 );
598 assert_eq!(
599 required_capability(&Method::POST, "/api/v1/sessions/{id}/execute"),
600 Capability("exec")
601 );
602 assert_eq!(
603 required_capability(&Method::GET, "/api/v1/sessions/{id}/ws"),
604 Capability("exec")
605 );
606
607 assert_eq!(
609 required_capability(&Method::GET, "/api/v1/sessions"),
610 Capability("session.read")
611 );
612 assert_eq!(
613 required_capability(&Method::POST, "/api/v1/sessions"),
614 Capability("session.manage")
615 );
616 assert_eq!(
617 required_capability(&Method::GET, "/api/v1/sessions/{id}"),
618 Capability("session.read")
619 );
620 assert_eq!(
621 required_capability(&Method::DELETE, "/api/v1/sessions/{id}"),
622 Capability("session.manage")
623 );
624 }
625
626 #[test]
627 fn test_required_capability_unknown_fails_closed() {
628 assert_eq!(
630 required_capability(&Method::GET, "/api/v1/unknown"),
631 RequiredCapability::Authenticated
632 );
633 }
634
635 #[test]
636 fn test_secure_router_creation() {
637 let state = AppState::new();
638 let security = SecurityConfig::secure().with_api_key("test-key");
639 let (router, auth_store, rate_limiter) = create_secure_router(state, security);
640
641 assert_eq!(auth_store.count(), 1);
642 assert!(auth_store.is_valid("test-key"));
643 assert!(rate_limiter.is_enabled());
644
645 drop(router);
647 }
648}