Skip to main content

rustbasic_core/
server.rs

1use axum::{Router, response::IntoResponse, ServiceExt, handler::HandlerWithoutStateExt};
2use tower_http::services::ServeDir;
3use tower_http::normalize_path::NormalizePathLayer;
4use tower::Layer;
5use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer, key_extractor::SmartIpKeyExtractor};
6use axum_session::{SessionLayer, SessionStore};
7use crate::app::Config;
8use crate::session_manager::RustBasicSessionStore;
9use crate::errors::ErrorController;
10use tower_governor::GovernorError;
11use std::net::SocketAddr;
12use sea_orm::DatabaseConnection;
13use std::sync::Arc;
14use std::process::Command;
15use std::time::Duration;
16use tower_livereload::LiveReloadLayer;
17
18#[derive(Clone)]
19#[allow(dead_code)]
20pub struct AppState {
21    pub db: DatabaseConnection,
22    pub config: Arc<Config>,
23}
24
25static EMBEDDED_PUBLIC_GET: std::sync::OnceLock<fn(&str) -> Option<rust_embed::EmbeddedFile>> = std::sync::OnceLock::new();
26
27pub fn set_embedded_public(f: fn(&str) -> Option<rust_embed::EmbeddedFile>) {
28    EMBEDDED_PUBLIC_GET.set(f).ok();
29}
30
31// Helper internal untuk menebak MIME type berdasarkan ekstensi file
32fn guess_mime(path: &str) -> &'static str {
33    if path.ends_with(".js") {
34        "application/javascript"
35    } else if path.ends_with(".css") {
36        "text/css"
37    } else if path.ends_with(".html") {
38        "text/html"
39    } else if path.ends_with(".png") {
40        "image/png"
41    } else if path.ends_with(".jpg") || path.ends_with(".jpeg") {
42        "image/jpeg"
43    } else if path.ends_with(".svg") {
44        "image/svg+xml"
45    } else if path.ends_with(".ico") {
46        "image/x-icon"
47    } else if path.ends_with(".json") {
48        "application/json"
49    } else if path.ends_with(".woff") {
50        "font/woff"
51    } else if path.ends_with(".woff2") {
52        "font/woff2"
53    } else {
54        "application/octet-stream"
55    }
56}
57
58// Custom handler untuk menyajikan file statis dari biner ter-embed
59async fn embedded_fallback_handler(uri: axum::http::Uri) -> impl IntoResponse {
60    let path = uri.path().trim_start_matches('/');
61    let file_path = if path.is_empty() { "index.html" } else { path };
62    let file = EMBEDDED_PUBLIC_GET.get().and_then(|f| f(file_path));
63
64    match file {
65        Some(content) => {
66            let mime = guess_mime(file_path);
67            axum::response::Response::builder()
68                .header(axum::http::header::CONTENT_TYPE, mime)
69                .body(axum::body::Body::from(content.data))
70                .unwrap()
71        }
72        None => ErrorController::not_found().await.into_response(),
73    }
74}
75
76pub async fn start_server(
77    cfg: Config, 
78    session_store: SessionStore<RustBasicSessionStore>,
79    db: DatabaseConnection,
80    app_router: Router<AppState>
81) {
82    // 0. Kill port jika sedang digunakan (Force Restart)
83    kill_port_if_in_use(cfg.app_port);
84
85    // 0.5 Set Timezone Global
86    unsafe {
87        std::env::set_var("TZ", &cfg.app_timezone);
88    }
89
90    // 1. Inisialisasi State
91    let state = AppState {
92        db,
93        config: Arc::new(cfg.clone()),
94    };
95
96    // 1.5 Konfigurasi Rate Limiting
97    let governor_conf = Arc::new(
98        GovernorConfigBuilder::default()
99            .key_extractor(SmartIpKeyExtractor)
100            .period(Duration::from_millis(1000 / cfg.app_limit_request))
101            .burst_size(cfg.app_limit_request as u32)
102            .finish()
103            .unwrap(),
104    );
105
106    // 2. Bangun Router
107    let app = Router::new().merge(app_router);
108    
109    // Tentukan Fallback Service Secara Dinamis (Disk vs Embedded)
110    let app = if cfg.app_debug {
111        let static_files = ServeDir::new("public");
112        app.fallback_service(static_files.not_found_service(ErrorController::not_found.into_service()))
113    } else {
114        app.fallback(embedded_fallback_handler)
115    };
116
117    let app = app
118        .layer(axum::middleware::from_fn(crate::middleware::security_headers::security_headers_middleware))
119        .layer(axum::middleware::from_fn(crate::middleware::logging::logging_middleware))
120        .layer(GovernorLayer::new(governor_conf))
121        .layer(SessionLayer::new(session_store))
122        .with_state(state);
123
124    // 2.5 Live Reload (Hanya aktif jika APP_DEBUG=true)
125    let app = if cfg.app_debug {
126        tracing::info!("🔄 Fitur Live Reload (Auto-refresh) diaktifkan.");
127        app.layer(LiveReloadLayer::new())
128    } else {
129        app
130    };
131    
132    // 2.6 Normalisasi Path (Menangani trailing slash /home/ -> /home)
133    let app = NormalizePathLayer::trim_trailing_slash().layer(app);
134
135    // 3. Tentukan Alamat
136    let addr_str = format!("{}:{}", cfg.app_host, cfg.app_port);
137    let addr: SocketAddr = addr_str.parse().expect("Alamat server tidak valid");
138    
139    tracing::info!("{} berjalan di: http://{}", cfg.app_name, addr);
140    
141    // 4. Jalankan Server dengan ConnectInfo agar IP bisa dideteksi
142    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
143    axum::serve(listener, ServiceExt::<axum::extract::Request>::into_make_service_with_connect_info::<SocketAddr>(app)).await.unwrap();
144}
145
146/// Membunuh proses yang menggunakan port tertentu agar tidak terjadi error "Address already in use"
147fn kill_port_if_in_use(port: u16) {
148    #[cfg(target_os = "macos")]
149    {
150        // Mencari PID yang menggunakan port tersebut
151        let output = Command::new("lsof")
152            .arg("-t")
153            .arg(format!("-i:{}", port))
154            .output();
155
156        if let Ok(out) = output {
157            let pid_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
158            if !pid_str.is_empty() {
159                tracing::warn!("Port {} sedang digunakan oleh PID {}. Membunuh proses...", port, pid_str);
160                
161                // Membunuh proses tersebut
162                for pid in pid_str.split('\n') {
163                    if !pid.is_empty() {
164                        let _ = Command::new("kill")
165                            .arg("-9")
166                            .arg(pid)
167                            .output();
168                    }
169                }
170
171                // Beri waktu sejenak agar OS melepas port (Penting agar tidak panic AddrInUse)
172                std::thread::sleep(std::time::Duration::from_millis(500));
173            }
174        }
175    }
176
177    #[cfg(target_os = "linux")]
178    {
179        let _ = Command::new("fuser")
180            .arg("-k")
181            .arg(format!("{}/tcp", port))
182            .output();
183    }
184
185    #[cfg(target_os = "windows")]
186    {
187        let output = Command::new("cmd")
188            .args(&["/C", &format!("netstat -ano | findstr :{}", port)])
189            .output();
190
191        if let Ok(out) = output {
192            let stdout = String::from_utf8_lossy(&out.stdout);
193            let mut found = false;
194            for line in stdout.lines() {
195                let parts: Vec<&str> = line.split_whitespace().collect();
196                if let Some(pid) = parts.last() {
197                    if pid.parse::<u32>().is_ok() {
198                        tracing::warn!("Port {} sedang digunakan oleh PID {}. Membunuh proses...", port, pid);
199                        let _ = Command::new("taskkill")
200                            .args(&["/F", "/PID", pid])
201                            .output();
202                        found = true;
203                    }
204                }
205            }
206            if found {
207                // Beri waktu sejenak agar OS melepas port
208                std::thread::sleep(std::time::Duration::from_millis(500));
209            }
210        }
211    }
212}
213
214/// Menangani error dari Rate Limiter (Governor) dengan tampilan HTML Premium
215#[allow(dead_code)]
216fn handle_governor_error(err: GovernorError) -> axum::response::Response {
217    match err {
218        GovernorError::TooManyRequests { wait_time, .. } => {
219            ErrorController::show(
220                429, 
221                &format!("Terlalu banyak permintaan. Silakan tunggu {} detik lagi.", wait_time)
222            ).into_response()
223        },
224        _ => ErrorController::show(500, "Terjadi kesalahan pada sistem pembatas request.").into_response(),
225    }
226}