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 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 kill_port_if_in_use(cfg.app_port);
72
73 unsafe {
75 std::env::set_var("TZ", &cfg.app_timezone);
76 }
77
78 let state = AppState {
80 db,
81 config: Arc::new(cfg.clone()),
82 };
83
84 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 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
146pub(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 let key = &r[1..r.len() - 1];
157 params.insert(key.to_string(), q.to_string());
158 } else if r.starts_with(':') {
159 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(), };
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 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}