1use super::ServerDataWithConfig;
2use crate::server::{config, logging::ServerLogger};
3use actix_web::{web, HttpResponse, Result as ActixResult};
4use serde::{Deserialize, Serialize};
5use serde_json::json;
6use std::collections::VecDeque;
7use std::sync::{Arc, LazyLock, Mutex};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11struct Message {
12 message: String,
13 from: String,
14 timestamp: String,
15 id: u32,
16}
17
18pub async fn status_handler(data: web::Data<ServerDataWithConfig>) -> ActixResult<HttpResponse> {
19 let uptime = SystemTime::now()
20 .duration_since(UNIX_EPOCH)
21 .unwrap_or_default()
22 .as_secs();
23 let server_dir = format!("www/{}-[{}]", data.server.name, data.server.port);
24
25 Ok(HttpResponse::Ok().json(json!({
26 "status": "running",
27 "server_id": data.server.id,
28 "server_name": data.server.name,
29 "port": data.server.port,
30 "proxy_port": data.proxy_https_port,
31 "server": config::get_server_name(),
32 "version": config::get_server_version(),
33 "uptime_seconds": uptime,
34 "static_files": true,
35 "template_system": true,
36 "hot_reload": true,
37 "websocket_endpoint": "/ws/hot-reload",
38 "server_directory": server_dir,
39 "log_file": format!(".rss/servers/{}-[{}].log", data.server.name, data.server.port),
40 "certificate_file": format!(".rss/certs/{}-{}.cert", data.server.name, data.server.port),
41 "private_key_file": format!(".rss/certs/{}-{}.key", data.server.name, data.server.port),
42 "urls": {
43 "http": format!("http://127.0.0.1:{}", data.server.port),
44 "proxy": format!("https://{}.localhost:{}", data.server.name, data.proxy_https_port)
45 }
46 })))
47}
48
49pub async fn info_handler(data: web::Data<ServerDataWithConfig>) -> ActixResult<HttpResponse> {
50 let server_dir = format!("www/{}-[{}]", data.server.name, data.server.port);
51
52 Ok(HttpResponse::Ok().json(json!({
53 "name": "Rush Sync Server",
54 "version": config::get_server_version(),
55 "server_id": data.server.id,
56 "server_name": data.server.name,
57 "port": data.server.port,
58 "proxy_port": data.proxy_https_port,
59 "static_files_enabled": true,
60 "template_system": "enabled",
61 "hot_reload_enabled": true,
62 "websocket_url": format!("ws://127.0.0.1:{}/ws/hot-reload", data.server.port),
63 "server_directory": server_dir,
64 "certificate": {
65 "cert_file": format!(".rss/certs/{}-{}.cert", data.server.name, data.server.port),
66 "key_file": format!(".rss/certs/{}-{}.key", data.server.name, data.server.port),
67 "common_name": format!("{}.localhost", data.server.name)
68 },
69 "urls": {
70 "http": format!("http://127.0.0.1:{}", data.server.port),
71 "proxy": format!("https://{}.localhost:{}", data.server.name, data.proxy_https_port),
72 "websocket": format!("ws://127.0.0.1:{}/ws/hot-reload", data.server.port)
73 },
74 "endpoints": [
75 { "path": "/", "method": "GET", "description": "Static files from server directory", "type": "static" },
76 { "path": "/.rss/favicon.svg", "method": "GET", "description": "SVG favicon", "type": "static" },
77 { "path": "/api/status", "method": "GET", "description": "Server status", "type": "api" },
78 { "path": "/api/info", "method": "GET", "description": "API information", "type": "api" },
79 { "path": "/api/metrics", "method": "GET", "description": "Server metrics", "type": "api" },
80 { "path": "/api/stats", "method": "GET", "description": "Request statistics", "type": "api" },
81 { "path": "/api/logs", "method": "GET", "description": "Live server logs", "type": "api" },
82 { "path": "/api/logs/raw", "method": "GET", "description": "Raw log data (JSON)", "type": "api" },
83 { "path": "/api/health", "method": "GET", "description": "Health check", "type": "api" },
84 { "path": "/ws/hot-reload", "method": "GET", "description": "WebSocket hot reload", "type": "websocket" }
85 ]
86 })))
87}
88
89pub async fn metrics_handler(data: web::Data<ServerDataWithConfig>) -> ActixResult<HttpResponse> {
90 let uptime = SystemTime::now()
91 .duration_since(UNIX_EPOCH)
92 .unwrap_or_default()
93 .as_secs();
94 let server_dir = format!("www/{}-[{}]", data.server.name, data.server.port);
95 let log_file_size = if let Ok(logger) = ServerLogger::new(&data.server.name, data.server.port) {
96 logger.get_log_file_size_bytes().unwrap_or(0)
97 } else {
98 0
99 };
100
101 let file_count = std::fs::read_dir(&server_dir)
102 .map(|entries| entries.count())
103 .unwrap_or(0);
104
105 Ok(HttpResponse::Ok().json(json!({
106 "server_id": data.server.id,
107 "server_name": data.server.name,
108 "port": data.server.port,
109 "uptime_seconds": uptime,
110 "status": "running",
111 "hot_reload": {
112 "enabled": true,
113 "websocket_url": format!("ws://127.0.0.1:{}/ws/hot-reload", data.server.port),
114 "watching_directory": server_dir,
115 "file_watcher": "active"
116 },
117 "static_files": {
118 "directory": server_dir,
119 "file_count": file_count,
120 "enabled": true,
121 "template_based": true
122 },
123 "logging": {
124 "file_size_bytes": log_file_size,
125 "enabled": true
126 },
127 "endpoints_count": 10,
128 "last_updated": uptime
129 })))
130}
131
132pub async fn stats_handler(data: web::Data<ServerDataWithConfig>) -> ActixResult<HttpResponse> {
133 let server_dir = format!("www/{}-[{}]", data.server.name, data.server.port);
134
135 let stats = if let Ok(logger) = ServerLogger::new(&data.server.name, data.server.port) {
136 logger.get_request_stats().await.unwrap_or_default()
137 } else {
138 Default::default()
139 };
140
141 Ok(HttpResponse::Ok().json(json!({
142 "server_id": data.server.id,
143 "server_name": data.server.name,
144 "server_directory": server_dir,
145 "total_requests": stats.total_requests,
146 "unique_ips": stats.unique_ips,
147 "error_requests": stats.error_requests,
148 "security_alerts": stats.security_alerts,
149 "performance_warnings": stats.performance_warnings,
150 "avg_response_time_ms": stats.avg_response_time,
151 "max_response_time_ms": stats.max_response_time,
152 "total_bytes_sent": stats.total_bytes_sent,
153 "uptime_seconds": SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
154 "hot_reload_status": "active"
155 })))
156}
157
158pub async fn health_handler(_data: web::Data<ServerDataWithConfig>) -> ActixResult<HttpResponse> {
159 let timestamp = SystemTime::now()
160 .duration_since(UNIX_EPOCH)
161 .unwrap_or_default()
162 .as_secs();
163
164 Ok(HttpResponse::Ok().json(json!({
165 "status": "healthy",
166 "timestamp": timestamp,
167 "uptime": "running",
168 "logging": "active",
169 "static_files": "enabled",
170 "template_system": "active",
171 "hot_reload": "active",
172 "file_watcher": "monitoring",
173 "config": "loaded from TOML"
174 })))
175}
176
177pub async fn ping_handler() -> ActixResult<HttpResponse> {
178 Ok(HttpResponse::Ok().json(json!({
179 "status": "pong",
180 "timestamp": SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
181 "server": "rush-sync-server",
182 "message": "Ping received successfully"
183 })))
184}
185
186static MESSAGES: LazyLock<Arc<Mutex<VecDeque<Message>>>> =
188 LazyLock::new(|| Arc::new(Mutex::new(VecDeque::new())));
189static MESSAGE_COUNTER: LazyLock<Arc<Mutex<u32>>> = LazyLock::new(|| Arc::new(Mutex::new(0)));
190
191pub async fn message_handler(body: web::Json<serde_json::Value>) -> ActixResult<HttpResponse> {
193 let message_text = body
194 .get("message")
195 .and_then(|v| v.as_str())
196 .unwrap_or("No message");
197
198 let from = body
199 .get("from")
200 .and_then(|v| v.as_str())
201 .unwrap_or("Unknown");
202
203 let timestamp = body
204 .get("timestamp")
205 .and_then(|v| v.as_str())
206 .map(|s| s.to_string())
207 .unwrap_or_else(|| chrono::Local::now().to_rfc3339());
208
209 let message_id = {
211 let mut messages = MESSAGES.lock().unwrap_or_else(|p| p.into_inner());
212 let mut counter = MESSAGE_COUNTER.lock().unwrap_or_else(|p| p.into_inner());
213 *counter += 1;
214 let id = *counter;
215
216 messages.push_back(Message {
217 message: message_text.to_string(),
218 from: from.to_string(),
219 timestamp: timestamp.to_string(),
220 id,
221 });
222
223 if messages.len() > 100 {
225 messages.pop_front();
226 }
227 id
228 };
229
230 log::info!("Message received from {}: {}", from, message_text);
231
232 Ok(HttpResponse::Ok().json(json!({
233 "status": "received",
234 "timestamp": SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
235 "message_id": message_id
236 })))
237}
238
239pub async fn messages_handler() -> ActixResult<HttpResponse> {
241 let messages = {
242 let messages_lock = MESSAGES.lock().unwrap_or_else(|p| p.into_inner());
243 messages_lock.iter().cloned().collect::<Vec<_>>()
244 };
245
246 Ok(HttpResponse::Ok().json(json!({
247 "messages": messages,
248 "count": messages.len(),
249 "status": "success"
250 })))
251}
252
253pub async fn close_browser_handler() -> ActixResult<HttpResponse> {
254 let html = r#"
255<script>
256setTimeout(() => { window.close(); }, 100);
257document.write('<h1>Server stopped - closing...</h1>');
258</script>
259"#;
260 Ok(HttpResponse::Ok().content_type("text/html").body(html))
261}
262
263pub async fn upload_file(
265 data: web::Data<ServerDataWithConfig>,
266 path: web::Path<String>,
267 body: web::Bytes,
268) -> ActixResult<HttpResponse> {
269 let file_path = path.into_inner();
270
271 if file_path.is_empty() {
272 return Ok(HttpResponse::BadRequest().json(json!({"error": "Path required"})));
273 }
274
275 let base_dir = crate::core::helpers::get_base_dir().map_err(|e| {
276 actix_web::error::ErrorInternalServerError(format!("Base dir error: {}", e))
277 })?;
278 let server_dir = base_dir
279 .join("www")
280 .join(format!("{}-[{}]", data.server.name, data.server.port));
281
282 if file_path.contains("..") {
284 return Ok(HttpResponse::Forbidden().json(json!({"error": "Path traversal blocked"})));
285 }
286
287 let target = server_dir.join(&file_path);
288
289 if let Some(parent) = target.parent() {
291 tokio::fs::create_dir_all(parent).await.map_err(|e| {
292 actix_web::error::ErrorInternalServerError(format!("Directory creation failed: {}", e))
293 })?;
294 }
295
296 let canonical_server = server_dir
298 .canonicalize()
299 .unwrap_or_else(|_| server_dir.clone());
300 if let Some(canonical_parent) = target.parent().and_then(|p| p.canonicalize().ok()) {
301 if !canonical_parent.starts_with(&canonical_server) {
302 return Ok(HttpResponse::Forbidden().json(json!({"error": "Path traversal blocked"})));
303 }
304 }
305
306 let size = body.len();
307 tokio::fs::write(&target, &body)
308 .await
309 .map_err(|e| actix_web::error::ErrorInternalServerError(format!("Write failed: {}", e)))?;
310
311 log::info!(
312 "File uploaded: {} ({} bytes) to {}-[{}]",
313 file_path,
314 size,
315 data.server.name,
316 data.server.port
317 );
318
319 Ok(HttpResponse::Ok().json(json!({
320 "status": "uploaded",
321 "path": file_path,
322 "size": size
323 })))
324}
325
326pub async fn list_files(
328 data: web::Data<ServerDataWithConfig>,
329 query: web::Query<std::collections::HashMap<String, String>>,
330) -> ActixResult<HttpResponse> {
331 let base_dir = crate::core::helpers::get_base_dir().map_err(|e| {
332 actix_web::error::ErrorInternalServerError(format!("Base dir error: {}", e))
333 })?;
334 let server_dir = base_dir
335 .join("www")
336 .join(format!("{}-[{}]", data.server.name, data.server.port));
337
338 let subpath = query.get("path").map(|s| s.as_str()).unwrap_or("");
339
340 if subpath.contains("..") {
341 return Ok(HttpResponse::Forbidden().json(json!({"error": "Path traversal blocked"})));
342 }
343
344 let target = if subpath.is_empty() {
345 server_dir.clone()
346 } else {
347 server_dir.join(subpath)
348 };
349
350 let canonical_server = server_dir
352 .canonicalize()
353 .unwrap_or_else(|_| server_dir.clone());
354 if let Ok(canonical_target) = target.canonicalize() {
355 if !canonical_target.starts_with(&canonical_server) {
356 return Ok(HttpResponse::Forbidden().json(json!({"error": "Path traversal blocked"})));
357 }
358 }
359
360 if !target.exists() {
361 return Ok(HttpResponse::NotFound().json(json!({"error": "Directory not found"})));
362 }
363
364 let mut entries = vec![];
365 if let Ok(mut dir) = tokio::fs::read_dir(&target).await {
366 while let Ok(Some(entry)) = dir.next_entry().await {
367 let name = entry.file_name().to_string_lossy().to_string();
368 let metadata = entry.metadata().await.ok();
369 entries.push(json!({
370 "name": name,
371 "is_dir": metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false),
372 "size": metadata.as_ref().map(|m| m.len()).unwrap_or(0),
373 }));
374 }
375 }
376
377 Ok(HttpResponse::Ok().json(json!({
378 "server_name": data.server.name,
379 "port": data.server.port,
380 "path": subpath,
381 "files": entries,
382 "count": entries.len()
383 })))
384}
385
386pub async fn delete_file(
388 data: web::Data<ServerDataWithConfig>,
389 path: web::Path<String>,
390) -> ActixResult<HttpResponse> {
391 let file_path = path.into_inner();
392
393 if file_path.is_empty() {
394 return Ok(HttpResponse::BadRequest().json(json!({"error": "Path required"})));
395 }
396
397 if file_path.contains("..") {
398 return Ok(HttpResponse::Forbidden().json(json!({"error": "Path traversal blocked"})));
399 }
400
401 let base_dir = crate::core::helpers::get_base_dir().map_err(|e| {
402 actix_web::error::ErrorInternalServerError(format!("Base dir error: {}", e))
403 })?;
404 let server_dir = base_dir
405 .join("www")
406 .join(format!("{}-[{}]", data.server.name, data.server.port));
407
408 let target = server_dir.join(&file_path);
409
410 let canonical_server = server_dir
412 .canonicalize()
413 .unwrap_or_else(|_| server_dir.clone());
414 if let Ok(canonical_target) = target.canonicalize() {
415 if !canonical_target.starts_with(&canonical_server) {
416 return Ok(HttpResponse::Forbidden().json(json!({"error": "Path traversal blocked"})));
417 }
418 }
419
420 if !target.exists() {
421 return Ok(HttpResponse::NotFound().json(json!({"error": "File not found"})));
422 }
423
424 if target.is_dir() {
425 tokio::fs::remove_dir_all(&target).await.map_err(|e| {
426 actix_web::error::ErrorInternalServerError(format!("Delete failed: {}", e))
427 })?;
428 } else {
429 tokio::fs::remove_file(&target).await.map_err(|e| {
430 actix_web::error::ErrorInternalServerError(format!("Delete failed: {}", e))
431 })?;
432 }
433
434 log::info!(
435 "File deleted: {} from {}-[{}]",
436 file_path,
437 data.server.name,
438 data.server.port
439 );
440
441 Ok(HttpResponse::Ok().json(json!({
442 "status": "deleted",
443 "path": file_path
444 })))
445}
446
447pub async fn settings_get_handler(
449 data: web::Data<ServerDataWithConfig>,
450) -> ActixResult<HttpResponse> {
451 let server_dir =
452 crate::server::settings::ServerSettings::get_server_dir(&data.server.name, data.server.port);
453 let settings = match server_dir {
454 Some(dir) => crate::server::settings::ServerSettings::load(&dir),
455 None => crate::server::settings::ServerSettings::default(),
456 };
457
458 Ok(HttpResponse::Ok().json(json!({
460 "custom_404_enabled": settings.custom_404_enabled,
461 "custom_404_path": settings.custom_404_path,
462 "pin_enabled": settings.pin_enabled,
463 "pin_set": !settings.pin_code.is_empty(),
464 })))
465}
466
467pub async fn settings_post_handler(
469 data: web::Data<ServerDataWithConfig>,
470 body: web::Json<serde_json::Value>,
471) -> ActixResult<HttpResponse> {
472 let server_dir =
473 crate::server::settings::ServerSettings::get_server_dir(&data.server.name, data.server.port);
474 let server_dir = match server_dir {
475 Some(dir) => dir,
476 None => {
477 return Ok(
478 HttpResponse::InternalServerError().json(json!({"error": "Server directory not found"}))
479 )
480 }
481 };
482
483 if !server_dir.exists() {
485 let _ = std::fs::create_dir_all(&server_dir);
486 }
487
488 let mut settings = crate::server::settings::ServerSettings::load(&server_dir);
489
490 if let Some(v) = body.get("custom_404_enabled").and_then(|v| v.as_bool()) {
492 settings.custom_404_enabled = v;
493 }
494 if let Some(v) = body.get("custom_404_path").and_then(|v| v.as_str()) {
495 let path = v.trim();
496 if !path.is_empty() && !path.contains("..") {
497 settings.custom_404_path = path.to_string();
498 }
499 }
500 if let Some(v) = body.get("pin_enabled").and_then(|v| v.as_bool()) {
501 settings.pin_enabled = v;
502 }
503 if let Some(v) = body.get("pin_code").and_then(|v| v.as_str()) {
504 let pin = v.trim();
505 if !pin.is_empty() {
506 settings.pin_code = crate::server::settings::ServerSettings::encode_pin(pin);
507 }
508 }
509
510 settings.ensure_404_page(&server_dir, &data.server.name);
512
513 match settings.save(&server_dir) {
514 Ok(_) => {
515 log::info!("Settings saved for {}-[{}]", data.server.name, data.server.port);
516 Ok(HttpResponse::Ok().json(json!({
517 "status": "saved",
518 "custom_404_enabled": settings.custom_404_enabled,
519 "custom_404_path": settings.custom_404_path,
520 "pin_enabled": settings.pin_enabled,
521 "pin_set": !settings.pin_code.is_empty(),
522 })))
523 }
524 Err(e) => {
525 log::error!("Failed to save settings: {}", e);
526 Ok(HttpResponse::InternalServerError().json(json!({"error": format!("Save failed: {}", e)})))
527 }
528 }
529}
530
531pub async fn pin_verify_handler(
533 data: web::Data<ServerDataWithConfig>,
534 body: web::Json<serde_json::Value>,
535) -> ActixResult<HttpResponse> {
536 let server_dir =
537 crate::server::settings::ServerSettings::get_server_dir(&data.server.name, data.server.port);
538 let settings = match server_dir {
539 Some(dir) => crate::server::settings::ServerSettings::load(&dir),
540 None => crate::server::settings::ServerSettings::default(),
541 };
542
543 let input_pin = body
544 .get("pin")
545 .and_then(|v| v.as_str())
546 .unwrap_or("");
547
548 if settings.verify_pin(input_pin) {
549 let token = format!("rss-pin-{}-{}", data.server.name, data.server.port);
551 Ok(HttpResponse::Ok()
552 .cookie(
553 actix_web::cookie::Cookie::build("rss_pin", &token)
554 .path("/")
555 .http_only(true)
556 .max_age(actix_web::cookie::time::Duration::minutes(3))
557 .finish(),
558 )
559 .json(json!({"status": "ok"})))
560 } else {
561 Ok(HttpResponse::Unauthorized().json(json!({"error": "Invalid PIN"})))
562 }
563}
564
565pub async fn pin_logout_handler() -> ActixResult<HttpResponse> {
567 Ok(HttpResponse::Ok()
568 .cookie(
569 actix_web::cookie::Cookie::build("rss_pin", "")
570 .path("/")
571 .http_only(true)
572 .max_age(actix_web::cookie::time::Duration::ZERO)
573 .finish(),
574 )
575 .json(json!({"status": "logged_out"})))
576}
577
578pub async fn acme_challenge_handler(path: web::Path<String>) -> ActixResult<HttpResponse> {
580 let token = path.into_inner();
581 if let Some(key_auth) = crate::server::acme::get_challenge_response(&token) {
582 Ok(HttpResponse::Ok().content_type("text/plain").body(key_auth))
583 } else {
584 Ok(HttpResponse::NotFound().body("Challenge not found"))
585 }
586}
587
588pub async fn acme_status_handler() -> ActixResult<HttpResponse> {
590 let status = crate::server::acme::get_acme_status();
591 Ok(HttpResponse::Ok().json(status))
592}
593
594pub async fn acme_dashboard_handler() -> ActixResult<HttpResponse> {
596 let status = crate::server::acme::get_acme_status();
597 let json_data = serde_json::to_string(&status)
598 .unwrap_or_else(|_| "{}".to_string())
599 .replace("</", "<\\/"); let html = crate::server::acme::ACME_DASHBOARD_HTML.replace("__ACME_DATA__", &json_data);
601 Ok(HttpResponse::Ok()
602 .content_type("text/html; charset=utf-8")
603 .body(html))
604}
605
606pub async fn analytics_handler() -> ActixResult<HttpResponse> {
608 let summary = crate::server::analytics::get_summary();
609 Ok(HttpResponse::Ok().json(summary))
610}
611
612pub async fn analytics_dashboard_handler() -> ActixResult<HttpResponse> {
614 let summary = crate::server::analytics::get_summary();
615 let json_data = serde_json::to_string(&summary)
616 .unwrap_or_else(|_| "{}".to_string())
617 .replace("</", "<\\/"); let html = crate::server::analytics::DASHBOARD_HTML.replace("__ANALYTICS_DATA__", &json_data);
619 Ok(HttpResponse::Ok()
620 .content_type("text/html; charset=utf-8")
621 .body(html))
622}