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