bamboo_server/server/
web_service.rs1use std::path::PathBuf;
2
3use actix_files as fs;
4use actix_web::{web, App, HttpServer};
5use tokio::sync::oneshot;
6use tracing::{error, info};
7
8use super::listeners::DEFAULT_WORKER_COUNT;
9use super::tls::build_rustls_config;
10use crate::app_state::AppState;
11use crate::config::{build_cors, build_rate_limiter, build_security_headers, is_loopback_bind};
12use crate::routes::{configure_routes, configure_routes_with_rate_limiting};
13use actix_governor::Governor;
14use bamboo_config::TlsConfig;
15
16pub struct WebService {
21 shutdown_tx: Option<oneshot::Sender<()>>,
22 server_handle: Option<tokio::task::JoinHandle<()>>,
23 app_state: Option<web::Data<AppState>>,
28 bamboo_home_dir: PathBuf,
30 port: u16,
31}
32
33impl WebService {
34 pub fn new(bamboo_home_dir: PathBuf) -> Self {
39 Self {
40 shutdown_tx: None,
41 server_handle: None,
42 app_state: None,
43 bamboo_home_dir,
44 port: 3456, }
46 }
47
48 pub async fn start(&mut self, port: u16) -> Result<(), String> {
50 self.start_with_bind(port, "127.0.0.1").await
51 }
52
53 pub async fn start_with_bind(&mut self, port: u16, bind: &str) -> Result<(), String> {
55 self.start_with_bind_tls(port, bind, None).await
56 }
57
58 pub async fn start_with_bind_tls(
62 &mut self,
63 port: u16,
64 bind: &str,
65 tls: Option<&TlsConfig>,
66 ) -> Result<(), String> {
67 info!("Starting web service...");
68 if self.server_handle.is_some() {
69 return Err("Web service is already running".to_string());
70 }
71
72 let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
73 self.port = port;
74
75 let app_state = web::Data::new(
76 AppState::new(self.bamboo_home_dir.clone())
77 .await
78 .map_err(|e| format!("Failed to initialize app state: {e}"))?,
79 );
80 self.app_state = Some(app_state.clone());
82 let bind_addr = bind.to_string();
83 let listen_addr = format!("{bind}:{port}");
84 let bind_for_log = bind_addr.clone();
85
86 let server = HttpServer::new(move || {
87 App::new()
88 .app_data(app_state.clone())
89 .wrap(build_cors(&bind_addr, port))
90 .configure(configure_routes) })
92 .workers(DEFAULT_WORKER_COUNT);
93
94 let server = match tls {
97 Some(tls) => server
98 .bind_rustls_0_23(&listen_addr, build_rustls_config(tls)?)
99 .map_err(|e| format!("Failed to bind TLS server: {e}"))?,
100 None => server
101 .bind(&listen_addr)
102 .map_err(|e| format!("Failed to bind server: {e}"))?,
103 }
104 .run();
105
106 let server_handle = tokio::spawn(async move {
107 tokio::select! {
108 result = server => {
109 if let Err(e) = result {
110 error!("Server error: {}", e);
111 }
112 }
113 _ = &mut shutdown_rx => {
114 info!("Web service shutdown signal received");
115 }
116 }
117 });
118
119 self.shutdown_tx = Some(shutdown_tx);
120 self.server_handle = Some(server_handle);
121
122 let scheme = if tls.is_some() { "https" } else { "http" };
123 info!(
124 "Web service started successfully on {scheme}://{}:{}",
125 bind_for_log, port
126 );
127 Ok(())
128 }
129
130 pub async fn start_with_bind_and_static(
133 &mut self,
134 port: u16,
135 bind: &str,
136 static_dir: PathBuf,
137 ) -> Result<(), String> {
138 self.start_with_bind_and_static_tls(port, bind, static_dir, None)
139 .await
140 }
141
142 pub async fn start_with_bind_and_static_tls(
145 &mut self,
146 port: u16,
147 bind: &str,
148 static_dir: PathBuf,
149 tls: Option<&TlsConfig>,
150 ) -> Result<(), String> {
151 info!("Starting web service with static frontend...");
152 if self.server_handle.is_some() {
153 return Err("Web service is already running".to_string());
154 }
155
156 let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
157 self.port = port;
158
159 let static_dir = static_dir
160 .canonicalize()
161 .map_err(|e| format!("Static directory not found: {:?}: {}", static_dir, e))?;
162 if !static_dir.is_dir() {
163 return Err(format!(
164 "Static path is not a directory: {}",
165 static_dir.display()
166 ));
167 }
168
169 let app_state = web::Data::new(
170 AppState::new(self.bamboo_home_dir.clone())
171 .await
172 .map_err(|e| format!("Failed to initialize app state: {e}"))?,
173 );
174 self.app_state = Some(app_state.clone());
176 let rate_limiter = build_rate_limiter();
181 let apply_rate_limit = !is_loopback_bind(bind);
182 let bind_addr = bind.to_string();
183 let listen_addr = format!("{bind}:{port}");
184 let bind_for_log = bind_addr.clone();
185
186 let server = HttpServer::new(move || {
187 App::new()
188 .app_data(web::JsonConfig::default().limit(25 * 1024 * 1024))
189 .app_data(web::PayloadConfig::new(30 * 1024 * 1024))
190 .app_data(app_state.clone())
191 .wrap(actix_web::middleware::Condition::new(
192 apply_rate_limit,
193 Governor::new(&rate_limiter),
194 ))
195 .wrap(build_cors(&bind_addr, port))
196 .wrap(build_security_headers())
197 .wrap(actix_web::middleware::from_fn(
201 crate::config::add_asset_cache_headers,
202 ))
203 .configure(configure_routes_with_rate_limiting)
204 .service(
205 fs::Files::new("/", static_dir.clone())
206 .index_file("index.html")
207 .prefer_utf8(true)
208 .disable_content_disposition()
209 .disable_content_disposition(),
210 )
211 })
212 .workers(DEFAULT_WORKER_COUNT);
213
214 let server = match tls {
217 Some(tls) => server
218 .bind_rustls_0_23(&listen_addr, build_rustls_config(tls)?)
219 .map_err(|e| format!("Failed to bind TLS server: {e}"))?,
220 None => server
221 .bind(&listen_addr)
222 .map_err(|e| format!("Failed to bind server: {e}"))?,
223 }
224 .run();
225
226 let server_handle = tokio::spawn(async move {
227 tokio::select! {
228 result = server => {
229 if let Err(e) = result {
230 error!("Server error: {}", e);
231 }
232 }
233 _ = &mut shutdown_rx => {
234 info!("Web service shutdown signal received");
235 }
236 }
237 });
238
239 self.shutdown_tx = Some(shutdown_tx);
240 self.server_handle = Some(server_handle);
241
242 let scheme = if tls.is_some() { "https" } else { "http" };
243 info!(
244 "Web service with static frontend started successfully on {scheme}://{}:{}",
245 bind_for_log, port
246 );
247 Ok(())
248 }
249
250 pub async fn stop(&mut self) -> Result<(), String> {
252 if let Some(shutdown_tx) = self.shutdown_tx.take() {
253 if shutdown_tx.send(()).is_err() {
254 error!("Failed to send shutdown signal");
255 return Err("Error sending shutdown signal".to_string());
256 }
257
258 if let Some(handle) = self.server_handle.take() {
259 if let Err(e) = handle.await {
260 error!("Error waiting for server shutdown: {}", e);
261 return Err(format!("Error waiting for server shutdown: {}", e));
262 }
263 }
264
265 if let Some(state) = self.app_state.take() {
270 state.shutdown().await;
271 }
272
273 info!("Web service stopped successfully");
274 }
275
276 Ok(())
277 }
278
279 pub fn is_running(&self) -> bool {
281 self.server_handle.is_some()
282 }
283
284 pub fn port(&self) -> u16 {
286 self.port
287 }
288}
289
290impl Drop for WebService {
291 fn drop(&mut self) {
292 if let Some(shutdown_tx) = self.shutdown_tx.take() {
293 let _ = shutdown_tx.send(());
294 }
295 if let Some(state) = self.app_state.take() {
300 state.mcp_proxy_shutdown.cancel();
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[tokio::test]
313 async fn stop_cancels_mcp_proxy_supervisor_token() {
314 let home = tempfile::TempDir::new().expect("tempdir");
315 let mut service = WebService::new(home.path().to_path_buf());
316 service
318 .start_with_bind(0, "127.0.0.1")
319 .await
320 .expect("web service starts");
321
322 let token = service
324 .app_state
325 .as_ref()
326 .expect("app_state retained after start")
327 .mcp_proxy_shutdown
328 .clone();
329 assert!(
330 !token.is_cancelled(),
331 "supervisor token is live while the service runs"
332 );
333
334 service.stop().await.expect("web service stops");
335
336 assert!(
337 token.is_cancelled(),
338 "stop() must cancel the MCP-proxy supervisor token so it terminates"
339 );
340 }
341}