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 kill_port_if_in_use(cfg.app_port);
63
64 unsafe {
66 std::env::set_var("TZ", &cfg.app_timezone);
67 }
68
69 let state = AppState {
71 db,
72 config: Arc::new(cfg.clone()),
73 };
74
75 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 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
137pub(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 let key = &r[1..r.len() - 1];
148 params.insert(key.to_string(), q.to_string());
149 } else if r.starts_with(':') {
150 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(), };
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 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}