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 serve_with_state(config: ServerConfig, state: AppState) -> crate::Result<()> {
405 let addr = config.bind_address();
406
407 let (router, auth_store, _rate_limiter) = create_secure_router(state, config.security.clone());
409
410 if auth_store.is_enabled() {
412 if auth_store.count() == 0 {
413 let key = crate::security::generate_api_key();
416 register_key(&auth_store, &key, &config.security.capabilities);
417 tracing::info!("Generated API key: {}", key);
418 }
419 tracing::info!(
420 "Authentication enabled with {} API key(s)",
421 auth_store.count()
422 );
423 } else {
424 tracing::warn!("Authentication is DISABLED - server is open to all requests");
425 }
426
427 tracing::info!("Starting shell-tunnel API server on {}", addr);
428
429 let listener = tokio::net::TcpListener::bind(&addr)
430 .await
431 .map_err(crate::error::ShellTunnelError::Io)?;
432
433 let service: IntoMakeServiceWithConnectInfo<Router, SocketAddr> =
435 router.into_make_service_with_connect_info::<SocketAddr>();
436
437 if config.graceful_shutdown {
438 axum::serve(listener, service)
440 .with_graceful_shutdown(shutdown_signal())
441 .await
442 .map_err(|e| {
443 crate::error::ShellTunnelError::Io(std::io::Error::other(e.to_string()))
444 })?;
445
446 tracing::info!("Server shutdown complete");
447 } else {
448 axum::serve(listener, service).await.map_err(|e| {
450 crate::error::ShellTunnelError::Io(std::io::Error::other(e.to_string()))
451 })?;
452 }
453
454 Ok(())
455}
456
457async fn shutdown_signal() {
459 let ctrl_c = async {
460 tokio::signal::ctrl_c()
461 .await
462 .expect("Failed to install Ctrl+C handler");
463 };
464
465 #[cfg(unix)]
466 let terminate = async {
467 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
468 .expect("Failed to install SIGTERM handler")
469 .recv()
470 .await;
471 };
472
473 #[cfg(not(unix))]
474 let terminate = std::future::pending::<()>();
475
476 tokio::select! {
477 _ = ctrl_c => {
478 tracing::info!("Received Ctrl+C, initiating graceful shutdown...");
479 }
480 _ = terminate => {
481 tracing::info!("Received SIGTERM, initiating graceful shutdown...");
482 }
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn test_server_config_default() {
492 let config = ServerConfig::default();
493 assert_eq!(config.host, "127.0.0.1");
494 assert_eq!(config.port, 3000);
495 assert_eq!(config.bind_address(), "127.0.0.1:3000");
496 assert!(config.graceful_shutdown);
497 }
498
499 #[test]
500 fn test_server_config_custom() {
501 let config = ServerConfig::new("0.0.0.0", 8080);
502 assert_eq!(config.bind_address(), "0.0.0.0:8080");
503 }
504
505 #[test]
506 fn test_server_config_with_security() {
507 let config = ServerConfig::new("0.0.0.0", 8080)
508 .with_security(SecurityConfig::secure().with_api_key("test-key"));
509
510 assert!(config.security.auth.enabled);
511 assert_eq!(config.security.api_keys.len(), 1);
512 }
513
514 #[test]
515 fn test_security_config_default() {
516 let config = SecurityConfig::default();
517 assert!(!config.auth.enabled); assert!(config.rate_limit.enabled);
519 }
520
521 #[test]
522 fn test_security_config_secure() {
523 let config = SecurityConfig::secure();
524 assert!(config.auth.enabled);
525 assert!(config.rate_limit.enabled);
526 }
527
528 #[test]
529 fn test_cors_restrictive_by_default() {
530 assert!(!SecurityConfig::default().cors.allow_any);
531 assert!(!SecurityConfig::secure().cors.allow_any);
532 assert!(cors_layer(&CorsConfig::default()).is_none());
533 }
534
535 #[test]
536 fn test_cors_allow_any_opt_in() {
537 let config = SecurityConfig::development().with_cors_allow_any();
538 assert!(config.cors.allow_any);
539 assert!(cors_layer(&config.cors).is_some());
540 }
541
542 #[test]
543 fn test_security_config_development() {
544 let config = SecurityConfig::development();
545 assert!(!config.auth.enabled);
546 assert!(config.rate_limit.enabled);
547 }
548
549 #[test]
550 fn test_router_creation() {
551 let _router = create_router();
552 }
554
555 #[test]
556 fn test_required_capability_mapping() {
557 use RequiredCapability::{Authenticated, Capability, Public};
558
559 assert_eq!(required_capability(&Method::GET, "/health"), Public);
561 assert_eq!(required_capability(&Method::GET, "/api/v1"), Authenticated);
562
563 assert_eq!(
565 required_capability(&Method::POST, "/api/v1/execute"),
566 Capability("exec")
567 );
568 assert_eq!(
569 required_capability(&Method::GET, "/api/v1/ws"),
570 Capability("exec")
571 );
572 assert_eq!(
573 required_capability(&Method::POST, "/api/v1/sessions/{id}/execute"),
574 Capability("exec")
575 );
576 assert_eq!(
577 required_capability(&Method::GET, "/api/v1/sessions/{id}/ws"),
578 Capability("exec")
579 );
580
581 assert_eq!(
583 required_capability(&Method::GET, "/api/v1/sessions"),
584 Capability("session.read")
585 );
586 assert_eq!(
587 required_capability(&Method::POST, "/api/v1/sessions"),
588 Capability("session.manage")
589 );
590 assert_eq!(
591 required_capability(&Method::GET, "/api/v1/sessions/{id}"),
592 Capability("session.read")
593 );
594 assert_eq!(
595 required_capability(&Method::DELETE, "/api/v1/sessions/{id}"),
596 Capability("session.manage")
597 );
598 }
599
600 #[test]
601 fn test_required_capability_unknown_fails_closed() {
602 assert_eq!(
604 required_capability(&Method::GET, "/api/v1/unknown"),
605 RequiredCapability::Authenticated
606 );
607 }
608
609 #[test]
610 fn test_secure_router_creation() {
611 let state = AppState::new();
612 let security = SecurityConfig::secure().with_api_key("test-key");
613 let (router, auth_store, rate_limiter) = create_secure_router(state, security);
614
615 assert_eq!(auth_store.count(), 1);
616 assert!(auth_store.is_valid("test-key"));
617 assert!(rate_limiter.is_enabled());
618
619 drop(router);
621 }
622}