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 sqlx::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<rust_embed::EmbeddedFile>> = std::sync::OnceLock::new();
24
25pub fn set_embedded_public(f: fn(&str) -> Option<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        let io = TokioIo::new(stream);
100        let state = state.clone();
101        let router = app_router.clone();
102        let peer_ip = peer_addr.ip().to_string();
103        let session_store = session_store.clone();
104
105        tokio::task::spawn(async move {
106            let service = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
107                let state = state.clone();
108                let router = router.clone();
109                let peer_ip = peer_ip.clone();
110                let session_store = session_store.clone();
111                async move {
112                    let res = handle_http_request(req, peer_ip, state, router, session_store).await;
113                    Ok::<_, Infallible>(res)
114                }
115            });
116
117            if let Err(err) = http1::Builder::new()
118                .serve_connection(io, service)
119                .await
120            {
121                tracing::debug!("Error serving connection: {:?}", err);
122            }
123        });
124    }
125}
126
127pub(crate) fn match_path(route_path: &str, req_path: &str) -> bool {
128    let r_parts: Vec<&str> = route_path.split('/').filter(|s| !s.is_empty()).collect();
129    let q_parts: Vec<&str> = req_path.split('/').filter(|s| !s.is_empty()).collect();
130    
131    if r_parts.len() != q_parts.len() {
132        return false;
133    }
134    
135    for (r, q) in r_parts.iter().zip(q_parts.iter()) {
136        if r.starts_with(':') || (r.starts_with('{') && r.ends_with('}')) {
137            continue;
138        }
139        if r != q {
140            return false;
141        }
142    }
143    true
144}
145
146/// Ekstrak nilai route parameter dari URL request.
147/// Contoh: route="/user/{id}", path="/user/42" → {"id": "42"}
148pub(crate) fn extract_params(route_path: &str, req_path: &str) -> std::collections::HashMap<String, String> {
149    let mut params = std::collections::HashMap::new();
150    let r_parts: Vec<&str> = route_path.split('/').filter(|s| !s.is_empty()).collect();
151    let q_parts: Vec<&str> = req_path.split('/').filter(|s| !s.is_empty()).collect();
152
153    for (r, q) in r_parts.iter().zip(q_parts.iter()) {
154        if r.starts_with('{') && r.ends_with('}') {
155            // Sintaks {param}
156            let key = &r[1..r.len() - 1];
157            params.insert(key.to_string(), q.to_string());
158        } else if r.starts_with(':') {
159            // Sintaks :param
160            let key = &r[1..];
161            params.insert(key.to_string(), q.to_string());
162        }
163    }
164    params
165}
166
167async fn serve_static_or_404(path: &str, state: &AppState) -> Response {
168    let clean_path = path.trim_start_matches('/');
169    let file_path = if clean_path.is_empty() { "index.html" } else { clean_path };
170
171    if state.config.app_debug {
172        let disk_path = std::path::Path::new("public").join(file_path);
173        if disk_path.exists() && disk_path.is_file() {
174            if let Ok(content) = std::fs::read(&disk_path) {
175                let mime = guess_mime(file_path);
176                return http::Response::builder()
177                    .header(http::header::CONTENT_TYPE, mime)
178                    .body(content)
179                    .unwrap();
180            }
181        }
182    } else {
183        if let Some(file) = EMBEDDED_PUBLIC_GET.get().and_then(|f| f(file_path)) {
184            let mime = guess_mime(file_path);
185            return http::Response::builder()
186                .header(http::header::CONTENT_TYPE, mime)
187                .body(file.data.to_vec())
188                .unwrap();
189        }
190    }
191
192    crate::errors::ErrorController::not_found().await
193}
194
195async fn handle_http_request(
196    hyper_req: hyper::Request<hyper::body::Incoming>,
197    peer_ip: String,
198    state: AppState,
199    router: Router<AppState>,
200    session_store: RustBasicSessionStore,
201) -> hyper::Response<http_body_util::Full<hyper::body::Bytes>> {
202    use http_body_util::BodyExt;
203    
204    let (parts, body) = hyper_req.into_parts();
205    let method = parts.method.clone();
206    let uri = parts.uri.clone();
207    let path = uri.path().to_string();
208    
209    let mut headers = std::collections::HashMap::new();
210    for (name, val) in parts.headers.iter() {
211        if let Ok(val_str) = val.to_str() {
212            headers.insert(name.as_str().to_lowercase(), val_str.to_string());
213        }
214    }
215    
216    let mut inputs = serde_json::json!({});
217    if let Some(query) = uri.query() {
218        if let Ok(params) = crate::serde_urlencoded::from_str::<std::collections::HashMap<String, String>>(query) {
219            for (k, v) in params {
220                inputs[k] = serde_json::json!(v);
221            }
222        }
223    }
224    
225    let body_bytes = body.collect().await.map(|c| c.to_bytes()).unwrap_or_default();
226    let content_type = headers.get("content-type").map(|s| s.as_str()).unwrap_or("");
227    if content_type.starts_with("application/json") {
228        if let Ok(json_val) = serde_json::from_slice::<serde_json::Value>(&body_bytes) {
229            if let serde_json::Value::Object(obj) = json_val {
230                for (k, v) in obj {
231                    inputs[k] = v;
232                }
233            }
234        }
235    } else if content_type.starts_with("application/x-www-form-urlencoded") {
236        if let Ok(params) = crate::serde_urlencoded::from_bytes::<std::collections::HashMap<String, String>>(&body_bytes) {
237            for (k, v) in params {
238                inputs[k] = serde_json::json!(v);
239            }
240        }
241    }
242    
243    let mut session_id = None;
244    if let Some(cookie_header) = headers.get("cookie") {
245        for cookie in cookie_header.split(';') {
246            let parts: Vec<&str> = cookie.split('=').map(|s| s.trim()).collect();
247            if parts.len() == 2 && parts[0] == "rustbasic_session" {
248                session_id = Some(parts[1].to_string());
249                break;
250            }
251        }
252    }
253    
254    let id = session_id.unwrap_or_else(|| {
255        crate::rand::distr::Alphanumeric.sample_string(&mut crate::rand::rng(), 40)
256    });
257    
258    let session_data = if let Some(payload_str) = session_store.load(&id).await {
259        serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&payload_str).unwrap_or_default()
260    } else {
261        serde_json::Map::new()
262    };
263    
264    let session = crate::session::Session::new(id.clone());
265    *session.data.lock().unwrap() = session_data;
266    
267    if session.get::<String>("_token").is_none() {
268        let new_token = crate::rand::distr::Alphanumeric.sample_string(&mut crate::rand::rng(), 40);
269        session.set("_token", new_token);
270    }
271    
272    let req = Request {
273        inputs,
274        method: method.clone(),
275        path: path.clone(),
276        headers,
277        session: session.clone(),
278        state: state.clone(),
279        ip_address: peer_ip,
280        params: std::collections::HashMap::new(), // diisi oleh RouteDispatcher saat match
281    };
282    
283    struct RouteDispatcher {
284        router: Router<AppState>,
285        state: AppState,
286    }
287
288    #[crate::async_trait]
289    impl crate::router::ErasedHandler for RouteDispatcher {
290        async fn call(&self, req: Request) -> Response {
291            let method = req.method.clone();
292            let path = req.path.clone();
293            
294            let mut matched_handler = None;
295            let mut matched_params = std::collections::HashMap::new();
296            for route in &self.router.routes {
297                if match_path(&route.path, &path) {
298                    for (m, h) in &route.handlers {
299                        if m == &method {
300                            matched_handler = Some(h.clone());
301                            matched_params = extract_params(&route.path, &path);
302                            break;
303                        }
304                    }
305                }
306                if matched_handler.is_some() {
307                    break;
308                }
309            }
310            
311            if let Some(handler) = matched_handler {
312                // Inject route params ke request
313                let mut req = req;
314                req.params = matched_params;
315                let mut chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::End(handler));
316                for mw in self.router.middlewares.iter().rev() {
317                    chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::Next(mw.clone(), chain));
318                }
319                chain.next(req).await
320            } else {
321                serve_static_or_404(&path, &self.state).await
322            }
323        }
324    }
325
326    let dispatcher = std::sync::Arc::new(RouteDispatcher {
327        router,
328        state: state.clone(),
329    });
330
331    let mut chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::End(dispatcher));
332    chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::Next(
333        crate::middleware::from_fn(crate::middleware::security_headers::security_headers_middleware),
334        chain,
335    ));
336    chain = std::sync::Arc::new(crate::middleware::MiddlewareChain::Next(
337        crate::middleware::from_fn(crate::middleware::logging::logging_middleware),
338        chain,
339    ));
340
341    let ip = req.ip_address.clone();
342    let res = chain.next(req).await;
343    
344    let final_session_data = session.data.lock().unwrap().clone();
345    if let Ok(session_json) = serde_json::to_string(&final_session_data) {
346        session_store.store(&id, &session_json, &ip).await;
347    }
348    
349    let (mut res_parts, res_body) = res.into_parts();
350    let cookie_val = format!("rustbasic_session={}; Path=/; HttpOnly; SameSite=Lax", id);
351    res_parts.headers.insert(
352        http::header::SET_COOKIE,
353        http::HeaderValue::from_str(&cookie_val).unwrap(),
354    );
355    
356    hyper::Response::from_parts(res_parts, http_body_util::Full::new(hyper::body::Bytes::from(res_body)))
357}
358
359fn kill_port_if_in_use(port: u16) {
360    #[cfg(target_os = "macos")]
361    {
362        let output = Command::new("lsof")
363            .arg("-t")
364            .arg(format!("-i:{}", port))
365            .output();
366
367        if let Ok(out) = output {
368            let pid_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
369            if !pid_str.is_empty() {
370                tracing::warn!("Port {} sedang digunakan oleh PID {}. Membunuh proses...", port, pid_str);
371                
372                for pid in pid_str.split('\n') {
373                    if !pid.is_empty() {
374                        let _ = Command::new("kill")
375                            .arg("-9")
376                            .arg(pid)
377                            .output();
378                    }
379                }
380
381                std::thread::sleep(std::time::Duration::from_millis(500));
382            }
383        }
384    }
385
386    #[cfg(target_os = "linux")]
387    {
388        let _ = Command::new("fuser")
389            .arg("-k")
390            .arg(format!("{}/tcp", port))
391            .output();
392    }
393
394    #[cfg(target_os = "windows")]
395    {
396        let output = Command::new("cmd")
397            .args(&["/C", &format!("netstat -ano | findstr :{}", port)])
398            .output();
399
400        if let Ok(out) = output {
401            let stdout = String::from_utf8_lossy(&out.stdout);
402            let mut found = false;
403            for line in stdout.lines() {
404                let parts: Vec<&str> = line.split_whitespace().collect();
405                if let Some(pid) = parts.last() {
406                    if pid.parse::<u32>().is_ok() {
407                        tracing::warn!("Port {} sedang digunakan oleh PID {}. Membunuh proses...", port, pid);
408                        let _ = Command::new("taskkill")
409                            .args(&["/F", "/PID", pid])
410                            .output();
411                        found = true;
412                    }
413                }
414            }
415            if found {
416                std::thread::sleep(std::time::Duration::from_millis(500));
417            }
418        }
419    }
420}