Skip to main content

rush_sync_server/server/handlers/web/
mod.rs

1pub mod api;
2pub mod assets;
3pub mod logs;
4pub mod server;
5pub mod templates;
6
7pub use api::*;
8pub use assets::*;
9pub use logs::*;
10pub use server::*;
11pub use templates::*;
12
13use crate::core::config::Config;
14use crate::server::logging::ServerLogger;
15use crate::server::middleware::{ApiKeyAuth, LoggingMiddleware, PinProtection, RateLimiter};
16use crate::server::tls::TlsManager;
17use crate::server::types::{ServerContext, ServerData, ServerInfo};
18use crate::server::watchdog::{get_watchdog_manager, ws_hot_reload};
19use actix_cors::Cors;
20use actix_web::{middleware, web, App, HttpServer};
21use std::path::PathBuf;
22use std::sync::Arc;
23use std::sync::OnceLock;
24use std::time::Duration;
25
26static GLOBAL_CONFIG: OnceLock<Config> = OnceLock::new();
27
28// Set the global config (called once at startup)
29pub fn set_global_config(config: Config) {
30    let _ = GLOBAL_CONFIG.set(config);
31}
32
33pub fn get_proxy_http_port() -> u16 {
34    // HTTP proxy runs on the configured proxy port (default 3000)
35    GLOBAL_CONFIG.get().map(|c| c.proxy.port).unwrap_or(3000)
36}
37
38pub fn get_proxy_https_port() -> u16 {
39    // HTTPS proxy runs on HTTP port + https_port_offset
40    GLOBAL_CONFIG
41        .get()
42        .map(|c| c.proxy.port + c.proxy.https_port_offset)
43        .unwrap_or(3443)
44}
45
46pub fn create_server_directory_and_files(
47    server_name: &str,
48    port: u16,
49) -> crate::core::error::Result<PathBuf> {
50    let base_dir = crate::core::helpers::get_base_dir()?;
51
52    let server_dir = base_dir
53        .join("www")
54        .join(format!("{}-[{}]", server_name, port));
55    std::fs::create_dir_all(&server_dir).map_err(crate::core::error::AppError::Io)?;
56
57    // Generate files from templates
58    let readme_template = include_str!("../templates/README.md");
59    let readme_content = readme_template
60        .replace("{{SERVER_NAME}}", server_name)
61        .replace("{{PORT}}", &port.to_string());
62    std::fs::write(server_dir.join("README.md"), readme_content)
63        .map_err(crate::core::error::AppError::Io)?;
64
65    let robots_template = include_str!("../templates/robots.txt");
66    let robots_content = robots_template.replace("{{PORT}}", &port.to_string());
67    std::fs::write(server_dir.join("robots.txt"), robots_content)
68        .map_err(crate::core::error::AppError::Io)?;
69
70    log::info!("Created development directory: {:?}", server_dir);
71    log::info!("Files created: README.md, robots.txt");
72    Ok(server_dir)
73}
74
75pub fn create_web_server(
76    ctx: &ServerContext,
77    server_info: ServerInfo,
78    config: &Config,
79) -> std::result::Result<actix_web::dev::ServerHandle, String> {
80    create_web_server_with_workers(ctx, server_info, config, None)
81}
82
83pub fn create_web_server_with_workers(
84    ctx: &ServerContext,
85    server_info: ServerInfo,
86    config: &Config,
87    workers_override: Option<usize>,
88) -> std::result::Result<actix_web::dev::ServerHandle, String> {
89    let server_id = server_info.id.clone();
90    let server_name = server_info.name.clone();
91    let server_port = server_info.port;
92    let servers_clone = Arc::clone(&ctx.servers);
93
94    let server_logger =
95        match ServerLogger::new_with_config(&server_name, server_info.port, &config.logging) {
96            Ok(logger) => Arc::new(logger),
97            Err(e) => return Err(format!("Logger creation failed: {}", e)),
98        };
99
100    if let Err(e) = crate::server::watchdog::start_server_watching(&server_name, server_port) {
101        log::warn!("Failed to start file watching for {}: {}", server_name, e);
102    } else {
103        log::info!(
104            "File watching started for server {} on port {}",
105            server_name,
106            server_port
107        );
108    }
109
110    let logger_for_start = server_logger.clone();
111    tokio::spawn(async move {
112        if let Err(e) = logger_for_start.log_server_start().await {
113            log::error!("Failed to log server start: {}", e);
114        }
115    });
116
117    // Build server data with proxy port configuration
118    let server_data = web::Data::new(ServerDataWithConfig {
119        server: ServerData {
120            id: server_id.clone(),
121            port: server_info.port,
122            name: server_name.clone(),
123        },
124        proxy_http_port: get_proxy_http_port(),
125        proxy_https_port: get_proxy_https_port(),
126    });
127
128    let server_logger_for_app = server_logger.clone();
129    let watchdog_manager = get_watchdog_manager().clone();
130
131    let tls_config = if config.server.enable_https && config.server.auto_cert {
132        match TlsManager::new(&config.server.cert_dir, config.server.cert_validity_days) {
133            Ok(tls_manager) => match tls_manager.get_rustls_config_for_domain(
134                &server_name,
135                server_port,
136                &config.server.production_domain,
137            ) {
138                Ok(rustls_config) => {
139                    log::info!("TLS certificate loaded for {}:{}", server_name, server_port);
140                    Some(rustls_config)
141                }
142                Err(e) => {
143                    log::error!("TLS setup failed: {}", e);
144                    None
145                }
146            },
147            Err(e) => {
148                log::error!("TLS manager creation failed: {}", e);
149                None
150            }
151        }
152    } else {
153        None
154    };
155
156    let production_domain = config.server.production_domain.clone();
157    let api_key = config.server.api_key.clone();
158    let rate_limit_rps = config.server.rate_limit_rps;
159    let rate_limit_enabled = config.server.rate_limit_enabled;
160    let pin_server_name = server_name.clone();
161    let pin_server_port = server_port;
162    let mut http_server = HttpServer::new(move || {
163        let prod_domain = production_domain.clone();
164        App::new()
165            .app_data(server_data.clone())
166            .app_data(web::Data::from(watchdog_manager.clone()))
167            .wrap(LoggingMiddleware::new(server_logger_for_app.clone()))
168            .wrap(RateLimiter::new(rate_limit_rps, rate_limit_enabled))
169            .wrap(ApiKeyAuth::new(api_key.clone()))
170            .wrap(PinProtection::new(&pin_server_name, pin_server_port))
171            .wrap(middleware::Compress::default())
172            .wrap(
173                Cors::default()
174                    .allowed_origin_fn(move |origin, _req_head| {
175                        let origin_str = origin.to_str().unwrap_or("");
176                        // Always allow local development
177                        let is_local =
178                            origin_str.contains("127.0.0.1") || origin_str.contains("localhost");
179                        if is_local {
180                            return true;
181                        }
182                        // Allow production domain if configured
183                        if prod_domain != "localhost" {
184                            return origin_str.contains(&prod_domain);
185                        }
186                        false
187                    })
188                    .allow_any_method()
189                    .allow_any_header()
190                    .max_age(3600),
191            )
192            // Assets
193            .route("/.rss/_reset.css", web::get().to(serve_global_reset_css))
194            .route("/.rss/style.css", web::get().to(serve_system_css))
195            .route("/.rss/favicon.svg", web::get().to(serve_system_favicon))
196            .route("/.rss/", web::get().to(serve_system_dashboard))
197            // Font Assets
198            .route("/.rss/fonts/{font}", web::get().to(serve_quicksand_font))
199            // JavaScript Assets
200            .route("/rss.js", web::get().to(serve_rss_js))
201            .route("/.rss/js/rush-app.js", web::get().to(serve_rush_app_js))
202            .route("/.rss/js/rush-api.js", web::get().to(serve_rush_api_js))
203            .route("/.rss/js/rush-ui.js", web::get().to(serve_rush_ui_js))
204            // API Routes (specific before generic)
205            .route("/api/status", web::get().to(status_handler))
206            .route("/api/health", web::get().to(health_handler))
207            .route("/api/info", web::get().to(info_handler))
208            .route("/api/metrics", web::get().to(metrics_handler))
209            .route("/api/stats", web::get().to(stats_handler))
210            .route("/api/ping", web::post().to(ping_handler))
211            .route("/api/message", web::post().to(message_handler))
212            .route("/api/messages", web::get().to(messages_handler))
213            .route("/api/close-browser", web::get().to(close_browser_handler))
214            .route("/api/logs", web::get().to(logs_handler))
215            .route("/api/logs/raw", web::get().to(logs_raw_handler))
216            .route("/api/acme/status", web::get().to(acme_status_handler))
217            .route("/api/acme/dashboard", web::get().to(acme_dashboard_handler))
218            .route("/api/analytics", web::get().to(analytics_handler))
219            .route("/api/analytics/dashboard", web::get().to(analytics_dashboard_handler))
220            // Settings API
221            .route("/api/settings", web::get().to(settings_get_handler))
222            .route("/api/settings", web::post().to(settings_post_handler))
223            .route("/api/pin/verify", web::post().to(pin_verify_handler))
224            .route("/api/pin/logout", web::post().to(pin_logout_handler))
225            // File Management API
226            .route("/api/files", web::get().to(list_files))
227            .route("/api/files/{path:.*}", web::put().to(upload_file))
228            .route("/api/files/{path:.*}", web::delete().to(delete_file))
229            // ACME Challenge (Let's Encrypt)
230            .route(
231                "/.well-known/acme-challenge/{token}",
232                web::get().to(acme_challenge_handler),
233            )
234            // WebSocket Routes
235            .route("/ws/hot-reload", web::get().to(ws_hot_reload))
236            // Fallback (must be last)
237            .default_service(web::route().to(serve_fallback_or_inject))
238    })
239    .workers(workers_override.unwrap_or(config.server.workers))
240    .shutdown_timeout(config.server.shutdown_timeout)
241    .disable_signals();
242
243    http_server = http_server
244        .bind((&*config.server.bind_address, server_info.port))
245        .map_err(|e| format!("HTTP bind failed: {}", e))?;
246
247    if let Some(tls_cfg) = tls_config {
248        let https_port = server_port + config.server.https_port_offset;
249        let bind_result = http_server.bind_rustls_021(
250            (&*config.server.bind_address, https_port),
251            tls_cfg.as_ref().clone(),
252        );
253        match bind_result {
254            Ok(server) => {
255                http_server = server;
256                log::info!("HTTPS active for {} on port {}", server_name, https_port);
257            }
258            Err(e) => {
259                log::error!(
260                    "HTTPS bind failed for {} on port {}: {}",
261                    server_name,
262                    https_port,
263                    e
264                );
265                log::info!("Continuing with HTTP only");
266                // http_server was consumed by bind_rustls_021, need to return error
267                return Err(format!("HTTPS bind failed: {}", e));
268            }
269        }
270    }
271
272    let server_result = http_server.run();
273    let server_handle = server_result.handle();
274
275    let server_id_for_thread = server_id.clone();
276    let logger_for_cleanup = server_logger.clone();
277    let startup_delay = config.server.startup_delay_ms;
278    let server_name_for_cleanup = server_name.clone();
279    let server_port_for_cleanup = server_port;
280
281    if config.proxy.enabled {
282        let proxy_manager = crate::server::shared::get_proxy_manager();
283        let proxy_server_name = server_name.clone();
284        let proxy_server_id = server_id.clone();
285        let proxy_server_port = server_port;
286        let startup_delay_clone = startup_delay;
287        let bind_addr = config.server.bind_address.clone();
288
289        tokio::spawn(async move {
290            tokio::time::sleep(tokio::time::Duration::from_millis(
291                startup_delay_clone + 100,
292            ))
293            .await;
294
295            if let Err(e) = proxy_manager
296                .add_route(&proxy_server_name, &proxy_server_id, proxy_server_port)
297                .await
298            {
299                log::error!(
300                    "Failed to register server {} with proxy: {}",
301                    proxy_server_name,
302                    e
303                );
304            } else {
305                log::info!(
306                    "Server {} registered with proxy: {} -> {}:{}",
307                    proxy_server_name,
308                    proxy_server_name,
309                    bind_addr,
310                    proxy_server_port
311                );
312            }
313        });
314    }
315
316    std::thread::spawn(move || {
317        // Use single-threaded runtime per server to minimize FD/thread overhead.
318        // Actix-web manages its own worker threads separately.
319        let rt = match tokio::runtime::Builder::new_current_thread()
320            .enable_all()
321            .build()
322        {
323            Ok(rt) => rt,
324            Err(e) => {
325                log::error!(
326                    "Failed to create runtime for server {}: {}",
327                    server_id_for_thread,
328                    e
329                );
330                if let Ok(mut servers) = servers_clone.write() {
331                    if let Some(server) = servers.get_mut(&server_id_for_thread) {
332                        server.status = crate::server::types::ServerStatus::Failed;
333                    }
334                }
335                return;
336            }
337        };
338        rt.block_on(async move {
339            match server_result.await {
340                Ok(_) => log::info!("Server {} ended normally", server_id_for_thread),
341                Err(e) => {
342                    log::error!("Server {} error: {}", server_id_for_thread, e);
343                    if let Ok(mut servers) = servers_clone.write() {
344                        if let Some(server) = servers.get_mut(&server_id_for_thread) {
345                            server.status = crate::server::types::ServerStatus::Failed;
346                        }
347                    }
348                }
349            }
350
351            if let Err(e) = crate::server::watchdog::stop_server_watching(
352                &server_name_for_cleanup,
353                server_port_for_cleanup,
354            ) {
355                log::warn!("Failed to stop file watching: {}", e);
356            } else {
357                log::info!(
358                    "File watching stopped for server {}",
359                    server_name_for_cleanup
360                );
361            }
362
363            if let Err(e) = logger_for_cleanup.log_server_stop().await {
364                log::error!("Failed to log server stop: {}", e);
365            }
366
367            if let Ok(mut servers) = servers_clone.write() {
368                if let Some(server) = servers.get_mut(&server_id_for_thread) {
369                    server.status = crate::server::types::ServerStatus::Stopped;
370                }
371            }
372        });
373    });
374
375    std::thread::sleep(Duration::from_millis(startup_delay));
376    Ok(server_handle)
377}
378
379#[derive(Debug, Clone)]
380pub struct ServerDataWithConfig {
381    pub server: ServerData,
382    pub proxy_http_port: u16,
383    pub proxy_https_port: u16,
384}