bws_web_server/server/
management_api.rs

1//! Management API Service for BWS Web Server
2//!
3//! This module provides a secure management API service that runs on localhost only.
4//! It handles administrative operations like configuration reload with proper security checks.
5
6use crate::config::ManagementConfig;
7use crate::handlers::ApiHandler;
8use crate::server::WebServerService;
9use async_trait::async_trait;
10use pingora::http::ResponseHeader;
11use pingora::prelude::*;
12use std::sync::Arc;
13
14/// Management API Service with localhost-only security
15#[derive(Clone)]
16pub struct ManagementApiService {
17    api_handler: ApiHandler,
18    config: ManagementConfig,
19}
20
21impl ManagementApiService {
22    /// Create a new Management API service
23    pub fn new(_web_service: Arc<WebServerService>, config: ManagementConfig) -> Self {
24        Self {
25            api_handler: ApiHandler::new(),
26            config,
27        }
28    }
29
30    /// Check if the request is from localhost
31    fn is_localhost_request(&self, session: &Session) -> bool {
32        if let Some(client_addr) = session.client_addr() {
33            if let Some(socket_addr) = client_addr.as_inet() {
34                let ip = socket_addr.ip();
35                return ip.is_loopback();
36            }
37        }
38        false
39    }
40
41    /// Check API key authentication
42    fn check_api_key(&self, session: &Session) -> bool {
43        if let Some(expected_key) = &self.config.api_key {
44            if let Some(provided_key) = session.get_header("X-API-Key") {
45                if let Ok(key_str) = provided_key.to_str() {
46                    return key_str == expected_key;
47                }
48            }
49            false
50        } else {
51            true // No API key configured, allow access
52        }
53    }
54
55    /// Send JSON error response
56    async fn send_error_response(
57        &self,
58        session: &mut Session,
59        status: u16,
60        message: &str,
61    ) -> Result<()> {
62        let error_body = format!(r#"{{"error": "{}"}}"#, message);
63        let mut header = ResponseHeader::build(status, Some(4))?;
64        header.insert_header("Content-Type", "application/json; charset=utf-8")?;
65        header.insert_header("Content-Length", error_body.len().to_string())?;
66        header.insert_header("Cache-Control", "no-cache, no-store, must-revalidate")?;
67
68        session
69            .write_response_header(Box::new(header), false)
70            .await?;
71        session
72            .write_response_body(Some(error_body.into_bytes().into()), true)
73            .await?;
74
75        Ok(())
76    }
77
78    /// Send JSON success response
79    async fn send_success_response(&self, session: &mut Session, message: &str) -> Result<()> {
80        let success_body = format!(r#"{{"message": "{}"}}"#, message);
81        let mut header = ResponseHeader::build(200, Some(4))?;
82        header.insert_header("Content-Type", "application/json; charset=utf-8")?;
83        header.insert_header("Content-Length", success_body.len().to_string())?;
84        header.insert_header("Cache-Control", "no-cache, no-store, must-revalidate")?;
85
86        session
87            .write_response_header(Box::new(header), false)
88            .await?;
89        session
90            .write_response_body(Some(success_body.into_bytes().into()), true)
91            .await?;
92
93        Ok(())
94    }
95}
96
97#[async_trait]
98impl ProxyHttp for ManagementApiService {
99    type CTX = ();
100
101    fn new_ctx(&self) -> Self::CTX {}
102
103    async fn upstream_peer(
104        &self,
105        _session: &mut Session,
106        _ctx: &mut Self::CTX,
107    ) -> Result<Box<HttpPeer>> {
108        // Management API doesn't proxy to upstream
109        Err(Error::new(ErrorType::InternalError).into_down())
110    }
111
112    async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> {
113        // Security check: only allow localhost requests
114        if !self.is_localhost_request(session) {
115            log::warn!(
116                "Management API access denied: request not from localhost ({})",
117                session
118                    .client_addr()
119                    .map(|addr| addr.to_string())
120                    .unwrap_or_else(|| "unknown".to_string())
121            );
122            self.send_error_response(session, 403, "Access denied: localhost only")
123                .await?;
124            return Ok(true);
125        }
126
127        // API key authentication check
128        if !self.check_api_key(session) {
129            log::warn!("Management API access denied: invalid or missing API key");
130            self.send_error_response(session, 401, "Unauthorized: invalid API key")
131                .await?;
132            return Ok(true);
133        }
134
135        let path = session.req_header().uri.path();
136        let method = session.req_header().method.as_str();
137
138        match (method, path) {
139            ("POST", "/api/config/reload") => {
140                log::info!("Management API: Config reload requested");
141
142                // Delegate to the main API handler for the actual reload logic
143                match self.api_handler.handle(session, None).await {
144                    Ok(_) => {
145                        log::info!("Configuration reloaded successfully via management API");
146                        self.send_success_response(session, "Configuration reloaded successfully")
147                            .await?;
148                    }
149                    Err(e) => {
150                        log::error!("Configuration reload failed via management API: {}", e);
151                        self.send_error_response(session, 500, "Configuration reload failed")
152                            .await?;
153                        return Ok(true);
154                    }
155                }
156                Ok(true)
157            }
158            _ => {
159                // Unknown endpoint
160                self.send_error_response(session, 404, "Endpoint not found")
161                    .await?;
162                Ok(true)
163            }
164        }
165    }
166
167    async fn upstream_request_filter(
168        &self,
169        _session: &mut Session,
170        _upstream_request: &mut pingora::http::RequestHeader,
171        _ctx: &mut Self::CTX,
172    ) -> Result<()> {
173        // Not used for management API
174        Ok(())
175    }
176
177    async fn response_filter(
178        &self,
179        _session: &mut Session,
180        _upstream_response: &mut pingora::http::ResponseHeader,
181        _ctx: &mut Self::CTX,
182    ) -> Result<()> {
183        // Not used for management API
184        Ok(())
185    }
186
187    async fn logging(
188        &self,
189        session: &mut Session,
190        _e: Option<&pingora::Error>,
191        _ctx: &mut Self::CTX,
192    ) {
193        if let Some(client_addr) = session.client_addr() {
194            log::info!(
195                "Management API request: {} {} from {}",
196                session.req_header().method,
197                session.req_header().uri.path(),
198                client_addr
199            );
200        }
201    }
202}