rush_sync_server/server/handlers/web/
mod.rs1pub 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, 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
28pub fn set_global_config(config: Config) {
30 let _ = GLOBAL_CONFIG.set(config);
31}
32
33pub fn get_proxy_http_port() -> u16 {
34 GLOBAL_CONFIG.get().map(|c| c.proxy.port).unwrap_or(3000)
36}
37
38pub fn get_proxy_https_port() -> u16 {
39 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 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 let server_id = server_info.id.clone();
81 let server_name = server_info.name.clone();
82 let server_port = server_info.port;
83 let servers_clone = Arc::clone(&ctx.servers);
84
85 let server_logger =
86 match ServerLogger::new_with_config(&server_name, server_info.port, &config.logging) {
87 Ok(logger) => Arc::new(logger),
88 Err(e) => return Err(format!("Logger creation failed: {}", e)),
89 };
90
91 if let Err(e) = crate::server::watchdog::start_server_watching(&server_name, server_port) {
92 log::warn!("Failed to start file watching for {}: {}", server_name, e);
93 } else {
94 log::info!(
95 "File watching started for server {} on port {}",
96 server_name,
97 server_port
98 );
99 }
100
101 let logger_for_start = server_logger.clone();
102 tokio::spawn(async move {
103 if let Err(e) = logger_for_start.log_server_start().await {
104 log::error!("Failed to log server start: {}", e);
105 }
106 });
107
108 let server_data = web::Data::new(ServerDataWithConfig {
110 server: ServerData {
111 id: server_id.clone(),
112 port: server_info.port,
113 name: server_name.clone(),
114 },
115 proxy_http_port: get_proxy_http_port(),
116 proxy_https_port: get_proxy_https_port(),
117 });
118
119 let server_logger_for_app = server_logger.clone();
120 let watchdog_manager = get_watchdog_manager().clone();
121
122 let tls_config = if config.server.enable_https && config.server.auto_cert {
123 match TlsManager::new(&config.server.cert_dir, config.server.cert_validity_days) {
124 Ok(tls_manager) => match tls_manager.get_rustls_config_for_domain(
125 &server_name,
126 server_port,
127 &config.server.production_domain,
128 ) {
129 Ok(rustls_config) => {
130 log::info!("TLS certificate loaded for {}:{}", server_name, server_port);
131 Some(rustls_config)
132 }
133 Err(e) => {
134 log::error!("TLS setup failed: {}", e);
135 None
136 }
137 },
138 Err(e) => {
139 log::error!("TLS manager creation failed: {}", e);
140 None
141 }
142 }
143 } else {
144 None
145 };
146
147 let production_domain = config.server.production_domain.clone();
148 let api_key = config.server.api_key.clone();
149 let rate_limit_rps = config.server.rate_limit_rps;
150 let rate_limit_enabled = config.server.rate_limit_enabled;
151 let mut http_server = HttpServer::new(move || {
152 let prod_domain = production_domain.clone();
153 App::new()
154 .app_data(server_data.clone())
155 .app_data(web::Data::new(watchdog_manager.clone()))
156 .wrap(LoggingMiddleware::new(server_logger_for_app.clone()))
157 .wrap(RateLimiter::new(rate_limit_rps, rate_limit_enabled))
158 .wrap(ApiKeyAuth::new(api_key.clone()))
159 .wrap(middleware::Compress::default())
160 .wrap(
161 Cors::default()
162 .allowed_origin_fn(move |origin, _req_head| {
163 let origin_str = origin.to_str().unwrap_or("");
164 let is_local =
166 origin_str.contains("127.0.0.1") || origin_str.contains("localhost");
167 if is_local {
168 return true;
169 }
170 if prod_domain != "localhost" {
172 return origin_str.contains(&prod_domain);
173 }
174 false
175 })
176 .allow_any_method()
177 .allow_any_header()
178 .max_age(3600),
179 )
180 .route("/.rss/_reset.css", web::get().to(serve_global_reset_css))
182 .route("/.rss/style.css", web::get().to(serve_system_css))
183 .route("/.rss/favicon.svg", web::get().to(serve_system_favicon))
184 .route("/.rss/", web::get().to(serve_system_dashboard))
185 .route("/.rss/fonts/{font}", web::get().to(serve_quicksand_font))
187 .route("/rss.js", web::get().to(serve_rss_js))
189 .route("/.rss/js/rush-app.js", web::get().to(serve_rush_app_js))
190 .route("/.rss/js/rush-api.js", web::get().to(serve_rush_api_js))
191 .route("/.rss/js/rush-ui.js", web::get().to(serve_rush_ui_js))
192 .route("/api/status", web::get().to(status_handler))
194 .route("/api/health", web::get().to(health_handler))
195 .route("/api/info", web::get().to(info_handler))
196 .route("/api/metrics", web::get().to(metrics_handler))
197 .route("/api/stats", web::get().to(stats_handler))
198 .route("/api/ping", web::post().to(ping_handler))
199 .route("/api/message", web::post().to(message_handler))
200 .route("/api/messages", web::get().to(messages_handler))
201 .route("/api/close-browser", web::get().to(close_browser_handler))
202 .route("/api/logs", web::get().to(logs_handler))
203 .route("/api/logs/raw", web::get().to(logs_raw_handler))
204 .route("/api/acme/status", web::get().to(acme_status_handler))
205 .route("/api/acme/dashboard", web::get().to(acme_dashboard_handler))
206 .route("/api/analytics", web::get().to(analytics_handler))
207 .route("/api/analytics/dashboard", web::get().to(analytics_dashboard_handler))
208 .route("/api/files", web::get().to(list_files))
210 .route("/api/files/{path:.*}", web::put().to(upload_file))
211 .route("/api/files/{path:.*}", web::delete().to(delete_file))
212 .route(
214 "/.well-known/acme-challenge/{token}",
215 web::get().to(acme_challenge_handler),
216 )
217 .route("/ws/hot-reload", web::get().to(ws_hot_reload))
219 .default_service(web::route().to(serve_fallback_or_inject))
221 })
222 .workers(config.server.workers)
223 .shutdown_timeout(config.server.shutdown_timeout)
224 .disable_signals();
225
226 http_server = http_server
227 .bind((&*config.server.bind_address, server_info.port))
228 .map_err(|e| format!("HTTP bind failed: {}", e))?;
229
230 if let Some(tls_cfg) = tls_config {
231 let https_port = server_port + config.server.https_port_offset;
232 let bind_result = http_server.bind_rustls_021(
233 (&*config.server.bind_address, https_port),
234 tls_cfg.as_ref().clone(),
235 );
236 match bind_result {
237 Ok(server) => {
238 http_server = server;
239 log::info!("HTTPS active for {} on port {}", server_name, https_port);
240 }
241 Err(e) => {
242 log::error!(
243 "HTTPS bind failed for {} on port {}: {}",
244 server_name,
245 https_port,
246 e
247 );
248 log::info!("Continuing with HTTP only");
249 return Err(format!("HTTPS bind failed: {}", e));
251 }
252 }
253 }
254
255 let server_result = http_server.run();
256 let server_handle = server_result.handle();
257
258 let server_id_for_thread = server_id.clone();
259 let logger_for_cleanup = server_logger.clone();
260 let startup_delay = config.server.startup_delay_ms;
261 let server_name_for_cleanup = server_name.clone();
262 let server_port_for_cleanup = server_port;
263
264 if config.proxy.enabled {
265 let proxy_manager = crate::server::shared::get_proxy_manager();
266 let proxy_server_name = server_name.clone();
267 let proxy_server_id = server_id.clone();
268 let proxy_server_port = server_port;
269 let startup_delay_clone = startup_delay;
270 let bind_addr = config.server.bind_address.clone();
271
272 tokio::spawn(async move {
273 tokio::time::sleep(tokio::time::Duration::from_millis(
274 startup_delay_clone + 100,
275 ))
276 .await;
277
278 if let Err(e) = proxy_manager
279 .add_route(&proxy_server_name, &proxy_server_id, proxy_server_port)
280 .await
281 {
282 log::error!(
283 "Failed to register server {} with proxy: {}",
284 proxy_server_name,
285 e
286 );
287 } else {
288 log::info!(
289 "Server {} registered with proxy: {} -> {}:{}",
290 proxy_server_name,
291 proxy_server_name,
292 bind_addr,
293 proxy_server_port
294 );
295 }
296 });
297 }
298
299 std::thread::spawn(move || {
300 let rt = match tokio::runtime::Runtime::new() {
301 Ok(rt) => rt,
302 Err(e) => {
303 log::error!(
304 "Failed to create runtime for server {}: {}",
305 server_id_for_thread,
306 e
307 );
308 if let Ok(mut servers) = servers_clone.write() {
309 if let Some(server) = servers.get_mut(&server_id_for_thread) {
310 server.status = crate::server::types::ServerStatus::Failed;
311 }
312 }
313 return;
314 }
315 };
316 rt.block_on(async move {
317 match server_result.await {
318 Ok(_) => log::info!("Server {} ended normally", server_id_for_thread),
319 Err(e) => {
320 log::error!("Server {} error: {}", server_id_for_thread, e);
321 if let Ok(mut servers) = servers_clone.write() {
322 if let Some(server) = servers.get_mut(&server_id_for_thread) {
323 server.status = crate::server::types::ServerStatus::Failed;
324 }
325 }
326 }
327 }
328
329 if let Err(e) = crate::server::watchdog::stop_server_watching(
330 &server_name_for_cleanup,
331 server_port_for_cleanup,
332 ) {
333 log::warn!("Failed to stop file watching: {}", e);
334 } else {
335 log::info!(
336 "File watching stopped for server {}",
337 server_name_for_cleanup
338 );
339 }
340
341 if let Err(e) = logger_for_cleanup.log_server_stop().await {
342 log::error!("Failed to log server stop: {}", e);
343 }
344
345 if let Ok(mut servers) = servers_clone.write() {
346 if let Some(server) = servers.get_mut(&server_id_for_thread) {
347 server.status = crate::server::types::ServerStatus::Stopped;
348 }
349 }
350 });
351 });
352
353 std::thread::sleep(Duration::from_millis(startup_delay));
354 Ok(server_handle)
355}
356
357#[derive(Debug, Clone)]
358pub struct ServerDataWithConfig {
359 pub server: ServerData,
360 pub proxy_http_port: u16,
361 pub proxy_https_port: u16,
362}