Skip to main content

rustbasic_core/
server.rs

1use crate::app::Config;
2use crate::tracing;
3use crate::session_manager::RustBasicSessionStore;
4use crate::router::{Router, Response};
5use crate::requests::Request;
6use std::net::SocketAddr;
7use crate::sql::AnyPool;
8use std::sync::Arc;
9use std::process::Command;
10use std::convert::Infallible;
11use tokio::net::TcpListener;
12use hyper::service::service_fn;
13use hyper_util::rt::TokioIo;
14use hyper::server::conn::http1;
15use crate::rand::distr::SampleString;
16
17#[derive(Clone)]
18pub struct AppState {
19    pub db: AnyPool,
20    pub config: Arc<Config>,
21}
22
23static EMBEDDED_PUBLIC_GET: std::sync::OnceLock<fn(&str) -> Option<crate::rust_embed::EmbeddedFile>> = std::sync::OnceLock::new();
24
25pub fn set_embedded_public(f: fn(&str) -> Option<crate::rust_embed::EmbeddedFile>) {
26    EMBEDDED_PUBLIC_GET.set(f).ok();
27}
28
29fn guess_mime(path: &str) -> &'static str {
30    if path.ends_with(".js") {
31        "application/javascript"
32    } else if path.ends_with(".css") {
33        "text/css"
34    } else if path.ends_with(".html") {
35        "text/html"
36    } else if path.ends_with(".png") {
37        "image/png"
38    } else if path.ends_with(".jpg") || path.ends_with(".jpeg") {
39        "image/jpeg"
40    } else if path.ends_with(".svg") {
41        "image/svg+xml"
42    } else if path.ends_with(".ico") {
43        "image/x-icon"
44    } else if path.ends_with(".json") {
45        "application/json"
46    } else if path.ends_with(".woff") {
47        "font/woff"
48    } else if path.ends_with(".woff2") {
49        "font/woff2"
50    } else {
51        "application/octet-stream"
52    }
53}
54
55pub async fn start_server(
56    cfg: Config, 
57    session_store: RustBasicSessionStore,
58    db: AnyPool,
59    app_router: Router<AppState>,
60) {
61    // Populate named routes
62    let mut routes_map = std::collections::HashMap::new();
63    for r in &app_router.routes {
64        if let Some(ref name) = r.name {
65            routes_map.insert(name.clone(), r.path.clone());
66        }
67    }
68    let _ = crate::router::NAMED_ROUTES.set(routes_map);
69
70    // 0. Kill port jika sedang digunakan (Force Restart)
71    kill_port_if_in_use(cfg.app_port);
72
73    // 0.5 Set Timezone Global
74    unsafe {
75        std::env::set_var("TZ", &cfg.app_timezone);
76    }
77
78    // 1. Inisialisasi State
79    let state = AppState {
80        db,
81        config: Arc::new(cfg.clone()),
82    };
83
84    // 3. Tentukan Alamat
85    let addr_str = format!("{}:{}", cfg.app_host, cfg.app_port);
86    let addr: SocketAddr = addr_str.parse().expect("Alamat server tidak valid");
87    
88    tracing::info!("{} berjalan di: http://{}", cfg.app_name, addr);
89    
90    // 4. Jalankan Server
91    let listener = TcpListener::bind(addr).await.unwrap();
92    
93    loop {
94        let (stream, peer_addr) = match listener.accept().await {
95            Ok(ok) => ok,
96            Err(_) => continue,
97        };
98        
99        // Optimasi latensi: Kirim paket TCP langsung tanpa buffering (disable Nagle's algorithm)
100        let _ = stream.set_nodelay(true);
101        
102        let io = TokioIo::new(stream);
103        let state = state.clone();
104        let router = app_router.clone();
105        let peer_ip = peer_addr.ip().to_string();
106        let session_store = session_store.clone();
107
108        tokio::task::spawn(async move {
109            let service = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
110                let state = state.clone();
111                let router = router.clone();
112                let peer_ip = peer_ip.clone();
113                let session_store = session_store.clone();
114                async move {
115                    let res = handle_http_request(req, peer_ip, state, router, session_store).await;
116                    Ok::<_, Infallible>(res)
117                }
118            });
119
120            if let Err(err) = http1::Builder::new()
121                .serve_connection(io, service)
122                .await
123            {
124                tracing::debug!("Error serving connection: {:?}", err);
125            }
126        });
127    }
128}
129
130pub(crate) fn match_path(route_path: &str, req_path: &str) -> bool {
131    let r_parts: Vec<&str> = route_path.split('/').filter(|s| !s.is_empty()).collect();
132    let q_parts: Vec<&str> = req_path.split('/').filter(|s| !s.is_empty()).collect();
133    
134    if r_parts.len() != q_parts.len() {
135        return false;
136    }
137    
138    for (r, q) in r_parts.iter().zip(q_parts.iter()) {
139        if r.starts_with(':') || (r.starts_with('{') && r.ends_with('}')) {
140            continue;
141        }
142        if r != q {
143            return false;
144        }
145    }
146    true
147}
148
149/// Ekstrak nilai route parameter dari URL request.
150/// Contoh: route="/user/{id}", path="/user/42" → {"id": "42"}
151pub(crate) fn extract_params(route_path: &str, req_path: &str) -> std::collections::HashMap<String, String> {
152    let mut params = std::collections::HashMap::new();
153    let r_parts: Vec<&str> = route_path.split('/').filter(|s| !s.is_empty()).collect();
154    let q_parts: Vec<&str> = req_path.split('/').filter(|s| !s.is_empty()).collect();
155
156    for (r, q) in r_parts.iter().zip(q_parts.iter()) {
157        if r.starts_with('{') && r.ends_with('}') {
158            // Sintaks {param}
159            let key = &r[1..r.len() - 1];
160            params.insert(key.to_string(), q.to_string());
161        } else if r.starts_with(':') {
162            // Sintaks :param
163            let key = &r[1..];
164            params.insert(key.to_string(), q.to_string());
165        }
166    }
167    params
168}
169
170async fn serve_static_or_404(path: &str, state: &AppState) -> Response {
171    let clean_path = path.trim_start_matches('/');
172    let file_path = if clean_path.is_empty() { "index.html" } else { clean_path };
173
174    if state.config.app_debug {
175        let disk_path = std::path::Path::new("public").join(file_path);
176        if disk_path.exists() && disk_path.is_file() {
177            if let Ok(content) = std::fs::read(&disk_path) {
178                let mime = guess_mime(file_path);
179                return http::Response::builder()
180                    .header(http::header::CONTENT_TYPE, mime)
181                    .body(content)
182                    .unwrap();
183            }
184        }
185    } else {
186        if let Some(file) = EMBEDDED_PUBLIC_GET.get().and_then(|f| f(file_path)) {
187            let mime = guess_mime(file_path);
188            return http::Response::builder()
189                .header(http::header::CONTENT_TYPE, mime)
190                .body(file.data.to_vec())
191                .unwrap();
192        }
193    }
194
195    crate::errors::ErrorController::not_found().await
196}
197
198async fn handle_http_request(
199    hyper_req: hyper::Request<hyper::body::Incoming>,
200    peer_ip: String,
201    state: AppState,
202    router: Router<AppState>,
203    session_store: RustBasicSessionStore,
204) -> hyper::Response<http_body_util::Full<hyper::body::Bytes>> {
205    use http_body_util::BodyExt;
206    
207    let (parts, body) = hyper_req.into_parts();
208    let method = parts.method.clone();
209    let uri = parts.uri.clone();
210    let path = uri.path().to_string();
211    
212    let mut headers = std::collections::HashMap::new();
213    for (name, val) in parts.headers.iter() {
214        if let Ok(val_str) = val.to_str() {
215            headers.insert(name.as_str().to_lowercase(), val_str.to_string());
216        }
217    }
218    
219    let mut inputs = serde_json::json!({});
220    if let Some(query) = uri.query() {
221        if let Ok(params) = crate::serde_urlencoded::from_str::<std::collections::HashMap<String, String>>(query) {
222            for (k, v) in params {
223                inputs[k] = serde_json::json!(v);
224            }
225        }
226    }
227    
228    let body_bytes = body.collect().await.map(|c| c.to_bytes()).unwrap_or_default();
229    let content_type = headers.get("content-type").map(|s| s.as_str()).unwrap_or("");
230    if content_type.starts_with("application/json") {
231        if let Ok(json_val) = serde_json::from_slice::<serde_json::Value>(&body_bytes) {
232            if let serde_json::Value::Object(obj) = json_val {
233                for (k, v) in obj {
234                    inputs[k] = v;
235                }
236            }
237        }
238    } else if content_type.starts_with("application/x-www-form-urlencoded") {
239        if let Ok(params) = crate::serde_urlencoded::from_bytes::<std::collections::HashMap<String, String>>(&body_bytes) {
240            for (k, v) in params {
241                inputs[k] = serde_json::json!(v);
242            }
243        }
244    }
245    
246    let mut session_id = None;
247    if let Some(cookie_header) = headers.get("cookie") {
248        for cookie in cookie_header.split(';') {
249            let parts: Vec<&str> = cookie.split('=').map(|s| s.trim()).collect();
250            if parts.len() == 2 && parts[0] == "rustbasic_session" {
251                session_id = Some(parts[1].to_string());
252                break;
253            }
254        }
255    }
256    
257    let id = session_id.unwrap_or_else(|| {
258        crate::rand::distr::Alphanumeric.sample_string(&mut crate::rand::rng(), 40)
259    });
260    
261    let session_data = if let Some(payload_str) = session_store.load(&id).await {
262        serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&payload_str).unwrap_or_default()
263    } else {
264        serde_json::Map::new()
265    };
266    
267    let session = crate::session::Session::new(id.clone());
268    *session.data.lock().unwrap() = session_data;
269    
270    if session.get::<String>("_token").is_none() {
271        let new_token = crate::rand::distr::Alphanumeric.sample_string(&mut crate::rand::rng(), 40);
272        session.set("_token", new_token);
273    }
274    
275    let req = Request {
276        inputs,
277        method: method.clone(),
278        path: path.clone(),
279        headers,
280        session: session.clone(),
281        state: state.clone(),
282        ip_address: peer_ip,
283        params: std::collections::HashMap::new(), // diisi oleh RouteDispatcher saat match
284    };
285    
286    struct RouteDispatcher {
287        router: Router<AppState>,
288        state: AppState,
289    }
290
291    #[crate::async_trait]
292    impl crate::router::ErasedHandler for RouteDispatcher {
293        async fn call(&self, req: Request) -> Response {
294            let method = req.method.clone();
295            let path = req.path.clone();
296            
297            let mut matched_handler = None;
298            let mut matched_params = std::collections::HashMap::new();
299            for route in &self.router.routes {
300                if match_path(&route.path, &path) {
301                    for (m, h) in &route.handlers {
302                        if m == &method {
303                            matched_handler = Some(h.clone());
304                            matched_params = extract_params(&route.path, &path);
305                            break;
306                        }
307                    }
308                }
309                if matched_handler.is_some() {
310                    break;
311                }
312            }
313            
314            if let Some(handler) = matched_handler {
315                // Inject route params ke request
316                let mut req = req;
317                req.params = matched_params;
318                let mut chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::End(handler));
319                for mw in self.router.middlewares.iter().rev() {
320                    chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::Next(mw.clone(), chain));
321                }
322                chain.next(req).await
323            } else {
324                serve_static_or_404(&path, &self.state).await
325            }
326        }
327    }
328
329    let dispatcher = std::sync::Arc::new(RouteDispatcher {
330        router,
331        state: state.clone(),
332    });
333
334    let mut chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::End(dispatcher));
335    chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::Next(
336        crate::middleware::from_fn(crate::middleware::security_headers::security_headers_middleware),
337        chain,
338    ));
339    chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::Next(
340        crate::middleware::from_fn(crate::middleware::logging::logging_middleware),
341        chain,
342    ));
343
344    let ip = req.ip_address.clone();
345    let res = chain.next(req).await;
346    
347    let final_session_data = session.data.lock().unwrap().clone();
348    if let Ok(session_json) = serde_json::to_string(&final_session_data) {
349        session_store.store(&id, &session_json, &ip).await;
350    }
351    
352    let (mut res_parts, res_body) = res.into_parts();
353    let cookie_val = format!("rustbasic_session={}; Path=/; HttpOnly; SameSite=Lax", id);
354    res_parts.headers.insert(
355        http::header::SET_COOKIE,
356        http::HeaderValue::from_str(&cookie_val).unwrap(),
357    );
358    
359    hyper::Response::from_parts(res_parts, http_body_util::Full::new(hyper::body::Bytes::from(res_body)))
360}
361
362fn kill_port_if_in_use(port: u16) {
363    #[cfg(target_os = "macos")]
364    {
365        let output = Command::new("lsof")
366            .arg("-t")
367            .arg(format!("-i:{}", port))
368            .output();
369
370        if let Ok(out) = output {
371            let pid_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
372            if !pid_str.is_empty() {
373                tracing::warn!("Port {} sedang digunakan oleh PID {}. Membunuh proses...", port, pid_str);
374                
375                for pid in pid_str.split('\n') {
376                    if !pid.is_empty() {
377                        let _ = Command::new("kill")
378                            .arg("-9")
379                            .arg(pid)
380                            .output();
381                    }
382                }
383
384                std::thread::sleep(std::time::Duration::from_millis(500));
385            }
386        }
387    }
388
389    #[cfg(target_os = "linux")]
390    {
391        let _ = Command::new("fuser")
392            .arg("-k")
393            .arg(format!("{}/tcp", port))
394            .output();
395    }
396
397    #[cfg(target_os = "windows")]
398    {
399        let output = Command::new("cmd")
400            .args(&["/C", &format!("netstat -ano | findstr :{}", port)])
401            .output();
402
403        if let Ok(out) = output {
404            let stdout = String::from_utf8_lossy(&out.stdout);
405            let mut found = false;
406            for line in stdout.lines() {
407                let parts: Vec<&str> = line.split_whitespace().collect();
408                if let Some(pid) = parts.last() {
409                    if pid.parse::<u32>().is_ok() {
410                        tracing::warn!("Port {} sedang digunakan oleh PID {}. Membunuh proses...", port, pid);
411                        let _ = Command::new("taskkill")
412                            .args(&["/F", "/PID", pid])
413                            .output();
414                        found = true;
415                    }
416                }
417            }
418            if found {
419                std::thread::sleep(std::time::Duration::from_millis(500));
420            }
421        }
422    }
423}