1use super::json::lua_table_to_json;
2#[cfg(feature = "server")]
3use super::json::lua_value_to_json;
4#[cfg(feature = "server")]
5use http_body_util::Full;
6#[cfg(feature = "server")]
7use hyper::body::{Bytes, Frame, Incoming};
8#[cfg(feature = "server")]
9use hyper::server::conn::http1;
10#[cfg(feature = "server")]
11use hyper::service::service_fn;
12#[cfg(feature = "server")]
13use hyper::{Request, Response, StatusCode};
14use mlua::{Lua, Table, UserData, Value};
15use rand::RngExt;
16#[cfg(feature = "server")]
17use std::cell::RefCell;
18#[cfg(feature = "server")]
19use std::collections::HashMap;
20#[cfg(feature = "server")]
21use std::pin::Pin;
22#[cfg(feature = "server")]
23use std::rc::Rc;
24#[cfg(feature = "server")]
25use std::task::{Context, Poll};
26#[cfg(feature = "server")]
27use tokio::net::TcpListener;
28#[cfg(feature = "server")]
29use tracing::error;
30
31#[cfg(feature = "server")]
43#[derive(Clone)]
44pub struct LuaAxumRouter(pub axum::Router);
45
46#[cfg(feature = "server")]
47impl mlua::UserData for LuaAxumRouter {}
48
49struct HttpClient(reqwest::Client);
50impl UserData for HttpClient {}
51
52pub fn register_http(lua: &Lua, client: reqwest::Client) -> mlua::Result<()> {
53 let http_table = lua.create_table()?;
54
55 for method in ["get", "post", "put", "patch", "delete"] {
56 let method_client = client.clone();
57 let method_name = method.to_string();
58
59 let func = lua.create_async_function(move |lua, args: mlua::MultiValue| {
60 let client = method_client.clone();
61 let method_name = method_name.clone();
62 async move { execute_http_request(&lua, &client, &method_name, args).await }
63 })?;
64 http_table.set(method, func)?;
65 }
66
67 let client_fn = lua.create_async_function(|lua, opts: Option<Table>| async move {
68 let mut builder = reqwest::Client::builder();
69
70 let timeout_secs: f64 = opts
71 .as_ref()
72 .and_then(|t| t.get::<f64>("timeout").ok())
73 .unwrap_or(30.0);
74 builder = builder.timeout(std::time::Duration::from_secs_f64(timeout_secs));
75
76 let follow_redirects: bool = opts
77 .as_ref()
78 .and_then(|t| t.get::<bool>("follow_redirects").ok())
79 .unwrap_or(true);
80 if !follow_redirects {
81 builder = builder.redirect(reqwest::redirect::Policy::none());
82 }
83
84 if let Some(ref opts_table) = opts {
85 if let Ok(ca_path) = opts_table.get::<String>("ca_cert_file") {
86 let pem = std::fs::read(&ca_path).map_err(|e| {
87 mlua::Error::runtime(format!(
88 "http.client: failed to read CA cert file {ca_path:?}: {e}"
89 ))
90 })?;
91 let cert = reqwest::Certificate::from_pem(&pem).map_err(|e| {
92 mlua::Error::runtime(format!("http.client: invalid PEM in {ca_path:?}: {e}"))
93 })?;
94 builder = builder.add_root_certificate(cert);
95 }
96 if let Ok(ca_pem) = opts_table.get::<String>("ca_cert") {
97 let cert = reqwest::Certificate::from_pem(ca_pem.as_bytes()).map_err(|e| {
98 mlua::Error::runtime(format!("http.client: invalid CA cert PEM: {e}"))
99 })?;
100 builder = builder.add_root_certificate(cert);
101 }
102 }
103
104 let client = builder.build().map_err(|e| {
105 mlua::Error::runtime(format!("http.client: failed to build client: {e}"))
106 })?;
107
108 let ud = lua.create_any_userdata(HttpClient(client))?;
109
110 let wrapper: Table = lua
111 .load(
112 r#"
113 local ud = ...
114 local obj = { _ud = ud }
115 setmetatable(obj, {
116 __index = {
117 get = function(self, url, opts)
118 return http._client_request(self._ud, "get", url, opts)
119 end,
120 post = function(self, url, body, opts)
121 return http._client_request(self._ud, "post", url, body, opts)
122 end,
123 put = function(self, url, body, opts)
124 return http._client_request(self._ud, "put", url, body, opts)
125 end,
126 patch = function(self, url, body, opts)
127 return http._client_request(self._ud, "patch", url, body, opts)
128 end,
129 delete = function(self, url, opts)
130 return http._client_request(self._ud, "delete", url, opts)
131 end,
132 }
133 })
134 return obj
135 "#,
136 )
137 .call(ud)?;
138
139 Ok(Value::Table(wrapper))
140 })?;
141 http_table.set("client", client_fn)?;
142
143 let client_request_fn =
144 lua.create_async_function(|lua, args: mlua::MultiValue| async move {
145 let mut args_iter = args.into_iter();
146
147 let client = match args_iter.next() {
148 Some(Value::UserData(ud)) => {
149 let hc = ud.borrow::<HttpClient>().map_err(|_| {
150 mlua::Error::runtime(
151 "http._client_request: first arg must be an http client",
152 )
153 })?;
154 hc.0.clone()
155 }
156 _ => {
157 return Err(mlua::Error::runtime(
158 "http._client_request: first arg must be an http client",
159 ));
160 }
161 };
162
163 let method_name: String = match args_iter.next() {
164 Some(Value::String(s)) => s.to_str()?.to_string(),
165 _ => {
166 return Err(mlua::Error::runtime(
167 "http._client_request: second arg must be method name",
168 ));
169 }
170 };
171
172 let remaining: mlua::MultiValue = args_iter.collect();
173 execute_http_request(&lua, &client, &method_name, remaining).await
174 })?;
175 http_table.set("_client_request", client_request_fn)?;
176
177 #[cfg(feature = "server")]
178 let serve_fn = lua.create_async_function(|lua, args: mlua::MultiValue| async move {
179 let mut args_iter = args.into_iter();
180
181 let port: u16 = match args_iter.next() {
182 Some(Value::Integer(n)) => n as u16,
183 _ => {
184 return Err::<(), _>(mlua::Error::runtime(
185 "http.serve: first argument must be a port number",
186 ));
187 }
188 };
189
190 let routes_table = match args_iter.next() {
191 Some(Value::Table(t)) => t,
192 _ => {
193 return Err::<(), _>(mlua::Error::runtime(
194 "http.serve: second argument must be a routes table",
195 ));
196 }
197 };
198
199 let routes = Rc::new(parse_routes(&routes_table)?);
200
201 let listener = TcpListener::bind(format!("0.0.0.0:{port}"))
202 .await
203 .map_err(|e| mlua::Error::runtime(format!("http.serve: bind failed: {e}")))?;
204
205 let actual_port = listener
207 .local_addr()
208 .map_err(|e| {
209 mlua::Error::runtime(format!("http.serve: failed to get local addr: {e}"))
210 })?
211 .port();
212 lua.globals().set("_SERVER_PORT", actual_port)?;
213
214 loop {
215 let (stream, addr) = listener
216 .accept()
217 .await
218 .map_err(|e| mlua::Error::runtime(format!("http.serve: accept failed: {e}")))?;
219 let peer_addr = addr.to_string();
220
221 let routes = routes.clone();
222 let lua_clone = lua.clone();
223
224 tokio::task::spawn_local(async move {
225 let io = hyper_util::rt::TokioIo::new(stream);
226 let routes = routes.clone();
227 let lua = lua_clone.clone();
228 let peer_addr = peer_addr.clone();
229
230 let service = service_fn(move |req: Request<Incoming>| {
231 let routes = routes.clone();
232 let lua = lua.clone();
233 let peer_addr = peer_addr.clone();
234 async move { handle_request(&lua, &routes, None, peer_addr, req).await }
235 });
236
237 if let Err(e) = http1::Builder::new()
238 .serve_connection(io, service)
239 .with_upgrades()
240 .await
241 && !e.to_string().contains("connection closed")
242 {
243 error!("http.serve: connection error: {e}");
244 }
245 });
246 }
247 })?;
248 #[cfg(feature = "server")]
249 http_table.set("serve", serve_fn)?;
250
251 #[cfg(feature = "server")]
268 let serve_with_extra_fn =
269 lua.create_async_function(|lua, args: mlua::MultiValue| async move {
270 let mut args_iter = args.into_iter();
271
272 let port: u16 = match args_iter.next() {
273 Some(Value::Integer(n)) => n as u16,
274 _ => {
275 return Err::<(), _>(mlua::Error::runtime(
276 "http.serve_with_extra: first argument must be a port number",
277 ));
278 }
279 };
280
281 let routes_table = match args_iter.next() {
282 Some(Value::Table(t)) => t,
283 _ => {
284 return Err::<(), _>(mlua::Error::runtime(
285 "http.serve_with_extra: second argument must be a routes table",
286 ));
287 }
288 };
289
290 let extra_router: axum::Router = match args_iter.next() {
291 Some(Value::UserData(ud)) => {
292 let r = ud.borrow::<LuaAxumRouter>().map_err(|_| {
293 mlua::Error::runtime(
294 "http.serve_with_extra: third argument must be a LuaAxumRouter userdata",
295 )
296 })?;
297 r.0.clone()
298 }
299 _ => {
300 return Err::<(), _>(mlua::Error::runtime(
301 "http.serve_with_extra: third argument must be a LuaAxumRouter userdata",
302 ));
303 }
304 };
305
306 let routes = Rc::new(parse_routes(&routes_table)?);
307
308 let listener = TcpListener::bind(format!("0.0.0.0:{port}"))
309 .await
310 .map_err(|e| {
311 mlua::Error::runtime(format!("http.serve_with_extra: bind failed: {e}"))
312 })?;
313
314 let actual_port = listener
315 .local_addr()
316 .map_err(|e| {
317 mlua::Error::runtime(format!(
318 "http.serve_with_extra: failed to get local addr: {e}"
319 ))
320 })?
321 .port();
322 lua.globals().set("_SERVER_PORT", actual_port)?;
323
324 loop {
325 let (stream, addr) = listener.accept().await.map_err(|e| {
326 mlua::Error::runtime(format!("http.serve_with_extra: accept failed: {e}"))
327 })?;
328 let peer_addr = addr.to_string();
329
330 let routes = routes.clone();
331 let lua_clone = lua.clone();
332 let extra_router = extra_router.clone();
333
334 tokio::task::spawn_local(async move {
335 let io = hyper_util::rt::TokioIo::new(stream);
336 let routes = routes.clone();
337 let lua = lua_clone.clone();
338 let peer_addr = peer_addr.clone();
339 let extra_router = extra_router.clone();
340
341 let service = service_fn(move |req: Request<Incoming>| {
342 let routes = routes.clone();
343 let lua = lua.clone();
344 let peer_addr = peer_addr.clone();
345 let extra_router = extra_router.clone();
346 async move {
347 handle_request(&lua, &routes, Some(extra_router), peer_addr, req).await
348 }
349 });
350
351 if let Err(e) = http1::Builder::new()
352 .serve_connection(io, service)
353 .with_upgrades()
354 .await
355 && !e.to_string().contains("connection closed")
356 {
357 error!("http.serve_with_extra: connection error: {e}");
358 }
359 });
360 }
361 })?;
362 #[cfg(feature = "server")]
363 http_table.set("serve_with_extra", serve_with_extra_fn)?;
364
365 let download_client = client.clone();
370 let download_fn = lua.create_async_function(move |_, args: mlua::MultiValue| {
371 let client = download_client.clone();
372 async move {
373 use futures_util::StreamExt;
374 use tokio::io::AsyncWriteExt;
375
376 let mut args_iter = args.into_iter();
377 let url: String = match args_iter.next() {
378 Some(mlua::Value::String(s)) => s.to_str()?.to_string(),
379 _ => {
380 return Err(mlua::Error::runtime(
381 "http.download: first arg must be url string",
382 ));
383 }
384 };
385 let path: String = match args_iter.next() {
386 Some(mlua::Value::String(s)) => s.to_str()?.to_string(),
387 _ => {
388 return Err(mlua::Error::runtime(
389 "http.download: second arg must be dest path string",
390 ));
391 }
392 };
393 let opts: Option<mlua::Table> = match args_iter.next() {
395 Some(mlua::Value::Table(t)) => Some(t),
396 _ => None,
397 };
398
399 let mut req = client.get(&url);
401 if let Some(ref t) = opts {
402 if let Ok(h) = t.get::<mlua::Table>("headers") {
403 for pair in h.pairs::<String, String>() {
404 let (k, v) = pair?;
405 req = req.header(&k, &v);
406 }
407 }
408 if let Ok(secs) = t.get::<f64>("timeout")
409 && secs.is_finite()
410 && secs > 0.0
411 {
412 req = req.timeout(std::time::Duration::from_secs_f64(secs));
413 }
414 }
415
416 const DEFAULT_MAX_SIZE: i64 = 1024 * 1024 * 1024;
419 let max_size: i64 = opts
420 .as_ref()
421 .and_then(|t| t.get::<i64>("max_size").ok())
422 .unwrap_or(DEFAULT_MAX_SIZE);
423
424 if let Some(parent) = std::path::Path::new(&path).parent()
426 && !parent.as_os_str().is_empty()
427 {
428 tokio::fs::create_dir_all(parent).await.map_err(|e| {
429 mlua::Error::runtime(format!("http.download: mkdir parent: {e}"))
430 })?;
431 }
432
433 let tmp = format!("{path}.tmp.{:016x}", rand::rng().random::<u64>());
437 let mut file = tokio::fs::File::create(&tmp).await.map_err(|e| {
438 mlua::Error::runtime(format!("http.download: create temp {tmp:?}: {e}"))
439 })?;
440
441 let do_download = async {
443 let resp = req
444 .send()
445 .await
446 .map_err(|e| mlua::Error::runtime(format!("http.download: request: {e}")))?;
447 if !resp.status().is_success() {
448 return Err(mlua::Error::runtime(format!(
449 "http.download: HTTP {} for {url}",
450 resp.status()
451 )));
452 }
453 let mut total: i64 = 0;
454 let mut stream = resp.bytes_stream();
455 while let Some(chunk) = stream.next().await {
456 let bytes = chunk
457 .map_err(|e| mlua::Error::runtime(format!("http.download: stream: {e}")))?;
458 total += bytes.len() as i64;
459 if max_size > 0 && total > max_size {
460 return Err(mlua::Error::runtime(format!(
461 "http.download: response exceeds max_size ({total} > {max_size} bytes) for {url}"
462 )));
463 }
464 file.write_all(&bytes)
465 .await
466 .map_err(|e| mlua::Error::runtime(format!("http.download: write: {e}")))?;
467 }
468 file.flush()
469 .await
470 .map_err(|e| mlua::Error::runtime(format!("http.download: flush: {e}")))?;
471 file.sync_all()
472 .await
473 .map_err(|e| mlua::Error::runtime(format!("http.download: fsync: {e}")))?;
474 drop(file); Ok(total)
476 };
477
478 match do_download.await {
479 Ok(total) => {
480 tokio::fs::rename(&tmp, &path).await.map_err(|e| {
481 mlua::Error::runtime(format!(
482 "http.download: rename {tmp:?} -> {path:?}: {e}"
483 ))
484 })?;
485 Ok(total)
486 }
487 Err(e) => {
488 let _ = tokio::fs::remove_file(&tmp).await;
489 Err(e)
490 }
491 }
492 }
493 })?;
494 http_table.set("download", download_fn)?;
495
496 lua.globals().set("http", http_table)?;
497 Ok(())
498}
499
500async fn execute_http_request(
501 lua: &Lua,
502 client: &reqwest::Client,
503 method_name: &str,
504 args: mlua::MultiValue,
505) -> mlua::Result<Value> {
506 let has_body = method_name != "get" && method_name != "delete";
507
508 let mut args_iter = args.into_iter();
509 let url: String = match args_iter.next() {
510 Some(Value::String(s)) => s.to_str()?.to_string(),
511 _ => {
512 return Err(mlua::Error::runtime(format!(
513 "http.{method_name}: first argument must be a URL string"
514 )));
515 }
516 };
517
518 let (mut body_str, mut auto_json, opts) = if has_body {
519 let (body, is_json) = match args_iter.next() {
520 Some(Value::String(s)) => (s.to_str()?.to_string(), false),
521 Some(Value::Table(t)) => {
522 let json_val = lua_table_to_json(&t)?;
523 let serialized = serde_json::to_string(&json_val).map_err(|e| {
524 mlua::Error::runtime(format!("http.{method_name}: JSON encode failed: {e}"))
525 })?;
526 (serialized, true)
527 }
528 Some(Value::Nil) | None => (String::new(), false),
529 _ => {
530 return Err(mlua::Error::runtime(format!(
531 "http.{method_name}: second argument must be a string, table, or nil"
532 )));
533 }
534 };
535 let opts = match args_iter.next() {
536 Some(Value::Table(t)) => Some(t),
537 Some(Value::Nil) | None => None,
538 _ => {
539 return Err(mlua::Error::runtime(format!(
540 "http.{method_name}: third argument must be a table or nil"
541 )));
542 }
543 };
544 (body, is_json, opts)
545 } else {
546 let opts = match args_iter.next() {
547 Some(Value::Table(t)) => Some(t),
548 Some(Value::Nil) | None => None,
549 _ => {
550 return Err(mlua::Error::runtime(format!(
551 "http.{method_name}: second argument must be a table or nil"
552 )));
553 }
554 };
555 (String::new(), false, opts)
556 };
557
558 if !has_body
565 && let Some(ref opts_table) = opts
566 && let Ok(body_val) = opts_table.get::<Value>("body")
567 {
568 match body_val {
569 Value::String(s) => body_str = s.to_str()?.to_string(),
570 Value::Table(t) => {
571 let json_val = lua_table_to_json(&t)?;
572 let serialized = serde_json::to_string(&json_val).map_err(|e| {
573 mlua::Error::runtime(format!("http.{method_name}: JSON encode failed: {e}"))
574 })?;
575 body_str = serialized;
576 auto_json = true;
577 }
578 Value::Nil => {}
579 _ => {
580 return Err(mlua::Error::runtime(format!(
581 "http.{method_name}: opts.body must be a string, table, or nil"
582 )));
583 }
584 }
585 }
586
587 let mut req = match method_name {
588 "get" => client.get(&url),
589 "post" => client.post(&url),
590 "put" => client.put(&url),
591 "patch" => client.patch(&url),
592 "delete" => client.delete(&url),
593 _ => {
594 return Err(mlua::Error::runtime(format!(
595 "http: unsupported method: {method_name}"
596 )));
597 }
598 };
599
600 if !body_str.is_empty() {
601 req = req.body(body_str);
602 }
603 if auto_json {
604 req = req.header("Content-Type", "application/json");
605 }
606 if let Some(ref opts_table) = opts
607 && let Ok(headers_table) = opts_table.get::<Table>("headers")
608 {
609 for pair in headers_table.pairs::<String, String>() {
610 let (k, v) = pair?;
611 req = req.header(k, v);
612 }
613 }
614
615 let resp = req
616 .send()
617 .await
618 .map_err(|e| mlua::Error::runtime(format!("http.{method_name} failed: {e}")))?;
619 let status = resp.status().as_u16();
620 let resp_headers = resp.headers().clone();
621
622 let is_sse = resp_headers
624 .get("content-type")
625 .and_then(|v| v.to_str().ok())
626 .is_some_and(|ct| ct.contains("text/event-stream"));
627
628 let on_event_callback = opts
629 .as_ref()
630 .and_then(|o| o.get::<mlua::Function>("on_event").ok());
631
632 if let (true, Some(callback)) = (is_sse, on_event_callback) {
633 let result = lua.create_table()?;
634 result.set("status", status)?;
635 let headers_out = lua.create_table()?;
636 for (name, value) in &resp_headers {
637 if let Ok(v) = value.to_str() {
638 headers_out.set(name.as_str().to_string(), v.to_string())?;
639 }
640 }
641 result.set("headers", headers_out)?;
642
643 let mut stream = resp.bytes_stream();
645 let mut buffer = String::new();
646
647 use futures_util::StreamExt;
648 while let Some(chunk) = stream.next().await {
649 let chunk = chunk.map_err(|e| {
650 mlua::Error::runtime(format!("http.{method_name}: SSE stream error: {e}"))
651 })?;
652 buffer.push_str(&String::from_utf8_lossy(&chunk));
653
654 while let Some(pos) = buffer.find("\n\n") {
656 let event_text = buffer[..pos].to_string();
657 buffer = buffer[pos + 2..].to_string();
658
659 if event_text.trim().is_empty() {
660 continue;
661 }
662
663 let event_table = lua.create_table()?;
664 for line in event_text.lines() {
665 if let Some(value) = line.strip_prefix("event: ") {
666 event_table.set("event", value.to_string())?;
667 } else if let Some(value) = line.strip_prefix("data: ") {
668 event_table.set("data", value.to_string())?;
669 } else if let Some(value) = line.strip_prefix("id: ") {
670 event_table.set("id", value.to_string())?;
671 } else if let Some(value) = line.strip_prefix("retry: ")
672 && let Ok(ms) = value.parse::<i64>()
673 {
674 event_table.set("retry", ms)?;
675 }
676 }
677
678 let action: Value = callback.call_async(Value::Table(event_table)).await?;
679 if let Value::String(s) = &action
681 && s.to_str()? == "close"
682 {
683 return Ok(Value::Table(result));
684 }
685 }
686 }
687
688 return Ok(Value::Table(result));
689 }
690
691 let body_bytes = resp.bytes().await.map_err(|e| {
696 mlua::Error::runtime(format!("http.{method_name}: reading body failed: {e}"))
697 })?;
698 let body = lua.create_string(&body_bytes)?;
699
700 let result = lua.create_table()?;
701 result.set("status", status)?;
702 result.set("body", body)?;
703
704 let headers_out = lua.create_table()?;
705 for (name, value) in &resp_headers {
706 if let Ok(v) = value.to_str() {
707 headers_out.set(name.as_str().to_string(), v.to_string())?;
708 }
709 }
710 result.set("headers", headers_out)?;
711
712 Ok(Value::Table(result))
713}
714
715#[cfg(feature = "server")]
717struct SseBody {
718 rx: tokio::sync::mpsc::Receiver<Bytes>,
719}
720
721#[cfg(feature = "server")]
722impl hyper::body::Body for SseBody {
723 type Data = Bytes;
724 type Error = std::convert::Infallible;
725
726 fn poll_frame(
727 mut self: Pin<&mut Self>,
728 cx: &mut Context<'_>,
729 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
730 match self.rx.poll_recv(cx) {
731 Poll::Ready(Some(bytes)) => Poll::Ready(Some(Ok(Frame::data(bytes)))),
732 Poll::Ready(None) => Poll::Ready(None),
733 Poll::Pending => Poll::Pending,
734 }
735 }
736}
737
738#[cfg(feature = "server")]
740fn format_sse_event(event_table: &Table) -> mlua::Result<String> {
741 let mut out = String::new();
742
743 if let Ok(Some(event)) = event_table.get::<Option<String>>("event") {
744 if event.contains('\n') || event.contains('\r') {
745 return Err(mlua::Error::runtime(
746 "SSE event name must not contain newlines",
747 ));
748 }
749 out.push_str("event: ");
750 out.push_str(&event);
751 out.push('\n');
752 }
753 if let Ok(Some(data)) = event_table.get::<Option<String>>("data") {
754 for line in data.split('\n') {
756 out.push_str("data: ");
757 out.push_str(line);
758 out.push('\n');
759 }
760 }
761 if let Ok(Some(id)) = event_table.get::<Option<String>>("id") {
762 if id.contains('\n') || id.contains('\r') {
763 return Err(mlua::Error::runtime("SSE id must not contain newlines"));
764 }
765 out.push_str("id: ");
766 out.push_str(&id);
767 out.push('\n');
768 }
769 if let Ok(Some(retry)) = event_table.get::<Option<i64>>("retry") {
770 out.push_str("retry: ");
771 out.push_str(&retry.to_string());
772 out.push('\n');
773 }
774
775 out.push('\n');
777 Ok(out)
778}
779
780#[cfg(feature = "server")]
781fn parse_routes(routes_table: &Table) -> mlua::Result<HashMap<(String, String), mlua::Function>> {
782 let mut routes = HashMap::new();
783 for method_pair in routes_table.pairs::<String, Table>() {
784 let (method, paths_table) = method_pair?;
785 let method_upper = method.to_uppercase();
786 for path_pair in paths_table.pairs::<String, mlua::Function>() {
787 let (path, func) = path_pair?;
788 routes.insert((method_upper.clone(), path), func);
789 }
790 }
791 Ok(routes)
792}
793
794#[cfg(feature = "server")]
804type ServerBody = axum::body::Body;
805
806#[cfg(feature = "server")]
807fn lookup_route<'a>(
808 routes: &'a HashMap<(String, String), mlua::Function>,
809 method: &str,
810 path: &str,
811) -> Option<&'a mlua::Function> {
812 let key = (method.to_string(), path.to_string());
813 if let Some(f) = routes.get(&key) {
814 return Some(f);
815 }
816 let mut search = path;
817 while let Some(pos) = search.rfind('/') {
818 let prefix = &search[..pos];
819 let wildcard_key = (method.to_string(), format!("{prefix}/*"));
820 if let Some(f) = routes.get(&wildcard_key) {
821 return Some(f);
822 }
823 if pos == 0 {
824 let root_key = (method.to_string(), "/*".to_string());
825 return routes.get(&root_key);
826 }
827 search = prefix;
828 }
829 None
830}
831
832#[cfg(feature = "server")]
833fn is_websocket_upgrade(headers: &[(String, String)]) -> bool {
834 headers.iter().any(|(k, v)| {
835 k.eq_ignore_ascii_case("upgrade") && v.to_ascii_lowercase().contains("websocket")
836 })
837}
838
839#[cfg(feature = "server")]
840fn validate_ws_request(headers: &[(String, String)]) -> Result<String, &'static str> {
841 let mut has_connection_upgrade = false;
842 let mut version_ok = false;
843 let mut key: Option<String> = None;
844 for (k, v) in headers {
845 match k.to_ascii_lowercase().as_str() {
846 "connection" if v.to_ascii_lowercase().contains("upgrade") => {
847 has_connection_upgrade = true;
848 }
849 "sec-websocket-version" if v.trim() == "13" => {
850 version_ok = true;
851 }
852 "sec-websocket-key" => {
853 key = Some(v.clone());
854 }
855 _ => {}
856 }
857 }
858 if !has_connection_upgrade {
859 return Err("missing Connection: Upgrade header");
860 }
861 if !version_ok {
862 return Err("Sec-WebSocket-Version must be 13");
863 }
864 key.ok_or("missing Sec-WebSocket-Key header")
865}
866
867#[cfg(feature = "server")]
868fn compute_ws_accept(key: &str) -> String {
869 use sha1::Digest;
870 const MAGIC: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
871 let mut hasher = sha1::Sha1::new();
872 hasher.update(key.as_bytes());
873 hasher.update(MAGIC);
874 let digest = hasher.finalize();
875 data_encoding::BASE64.encode(&digest)
876}
877
878#[cfg(feature = "server")]
890async fn forward_to_axum_router(
891 mut router: axum::Router,
892 req: Request<Incoming>,
893) -> Result<Response<ServerBody>, hyper::Error> {
894 use tower::Service;
895 match <axum::Router as Service<Request<Incoming>>>::call(&mut router, req).await {
900 Ok(resp) => Ok(resp),
901 Err(_) => unreachable!("axum::Router::call is Infallible"),
902 }
903}
904
905#[cfg(feature = "server")]
906async fn handle_request(
907 lua: &Lua,
908 routes: &HashMap<(String, String), mlua::Function>,
909 extra_router: Option<axum::Router>,
910 peer_addr: String,
911 req: Request<Incoming>,
912) -> Result<Response<ServerBody>, hyper::Error> {
913 let method = req.method().to_string();
914 let path = req.uri().path().to_string();
915 let query = req.uri().query().unwrap_or("").to_string();
916 let headers: Vec<(String, String)> = req
917 .headers()
918 .iter()
919 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
920 .collect();
921
922 let is_ws = is_websocket_upgrade(&headers);
923
924 let handler = match lookup_route(routes, &method, &path) {
925 Some(h) => h.clone(),
926 None => {
927 if let Some(router) = extra_router {
932 return forward_to_axum_router(router, req).await;
933 }
934 return Ok(Response::builder()
935 .status(StatusCode::NOT_FOUND)
936 .header("content-type", "text/plain")
937 .body(axum::body::Body::new(Full::new(Bytes::from("not found"))))
938 .unwrap());
939 }
940 };
941
942 if is_ws {
943 let lua_resp =
944 match build_lua_request_and_call(lua, &handler, &method, &path, &query, &headers, "")
945 .await
946 {
947 Ok(t) => t,
948 Err(e) => {
949 return Ok(Response::builder()
950 .status(StatusCode::INTERNAL_SERVER_ERROR)
951 .header("content-type", "text/plain")
952 .body(axum::body::Body::new(Full::new(Bytes::from(format!(
953 "handler error: {e}"
954 )))))
955 .unwrap());
956 }
957 };
958
959 if let Ok(Some(ws_fn)) = lua_resp.get::<Option<mlua::Function>>("ws") {
960 return build_ws_upgrade_response(lua, &headers, lua_resp, ws_fn, peer_addr, req);
961 }
962
963 return lua_response_to_http(lua, &lua_resp);
964 }
965
966 let body_bytes = match http_body_util::BodyExt::collect(req.into_body()).await {
967 Ok(collected) => collected.to_bytes(),
968 Err(_) => Bytes::new(),
969 };
970 let body_str = String::from_utf8_lossy(&body_bytes).to_string();
971
972 match build_lua_request_and_call(lua, &handler, &method, &path, &query, &headers, &body_str)
973 .await
974 {
975 Ok(lua_resp) => lua_response_to_http(lua, &lua_resp),
976 Err(e) => Ok(Response::builder()
977 .status(StatusCode::INTERNAL_SERVER_ERROR)
978 .header("content-type", "text/plain")
979 .body(axum::body::Body::new(Full::new(Bytes::from(format!(
980 "handler error: {e}"
981 )))))
982 .unwrap()),
983 }
984}
985
986#[cfg(feature = "server")]
987fn build_ws_upgrade_response(
988 lua: &Lua,
989 headers: &[(String, String)],
990 resp_table: Table,
991 ws_fn: mlua::Function,
992 peer_addr: String,
993 req: Request<Incoming>,
994) -> Result<Response<ServerBody>, hyper::Error> {
995 let key = match validate_ws_request(headers) {
996 Ok(k) => k,
997 Err(msg) => {
998 return Ok(Response::builder()
999 .status(StatusCode::BAD_REQUEST)
1000 .header("content-type", "text/plain")
1001 .body(axum::body::Body::new(Full::new(Bytes::from(format!(
1002 "websocket upgrade rejected: {msg}"
1003 )))))
1004 .unwrap());
1005 }
1006 };
1007 let accept = compute_ws_accept(&key);
1008
1009 let mut builder = Response::builder()
1010 .status(StatusCode::SWITCHING_PROTOCOLS)
1011 .header(hyper::header::UPGRADE, "websocket")
1012 .header(hyper::header::CONNECTION, "Upgrade")
1013 .header("sec-websocket-accept", accept);
1014
1015 if let Ok(Some(headers_table)) = resp_table.get::<Option<Table>>("headers") {
1018 for pair in headers_table.pairs::<String, mlua::String>().flatten() {
1019 let (k, v) = pair;
1020 let kl = k.to_ascii_lowercase();
1021 if matches!(
1022 kl.as_str(),
1023 "upgrade" | "connection" | "sec-websocket-accept"
1024 ) {
1025 continue;
1026 }
1027 if let Ok(s) = v.to_str() {
1028 builder = builder.header(&k, s.as_ref());
1029 }
1030 }
1031 }
1032
1033 let response = builder
1034 .body(axum::body::Body::new(Full::new(Bytes::new())))
1035 .unwrap();
1036
1037 let lua_clone = lua.clone();
1038 tokio::task::spawn_local(async move {
1039 let upgraded = match hyper::upgrade::on(req).await {
1040 Ok(u) => u,
1041 Err(e) => {
1042 error!("http.serve: ws upgrade failed: {e}");
1043 return;
1044 }
1045 };
1046 let io = hyper_util::rt::TokioIo::new(upgraded);
1047 let stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
1048 io,
1049 tokio_tungstenite::tungstenite::protocol::Role::Server,
1050 None,
1051 )
1052 .await;
1053 let conn = super::ws::WsServerConn::new(stream, peer_addr);
1054 let ud = match lua_clone.create_userdata(conn) {
1055 Ok(u) => u,
1056 Err(e) => {
1057 error!("http.serve: ws userdata creation failed: {e}");
1058 return;
1059 }
1060 };
1061 if let Err(e) = ws_fn.call_async::<()>(ud).await
1062 && !e.to_string().contains("conn:read: ")
1063 {
1064 error!("http.serve: ws handler error: {e}");
1065 }
1066 });
1067
1068 Ok(response)
1069}
1070
1071#[cfg(feature = "server")]
1072async fn build_lua_request_and_call(
1073 lua: &Lua,
1074 handler: &mlua::Function,
1075 method: &str,
1076 path: &str,
1077 query: &str,
1078 headers: &[(String, String)],
1079 body: &str,
1080) -> mlua::Result<Table> {
1081 let req_table = lua.create_table()?;
1082 req_table.set("method", method.to_string())?;
1083 req_table.set("path", path.to_string())?;
1084 req_table.set("query", query.to_string())?;
1085 req_table.set("body", body.to_string())?;
1086
1087 let params_table = lua.create_table()?;
1092 if !query.is_empty() {
1093 for (key, value) in url::form_urlencoded::parse(query.as_bytes()) {
1094 params_table.set(key.into_owned(), value.into_owned())?;
1095 }
1096 }
1097 req_table.set("params", params_table)?;
1098
1099 let headers_table = lua.create_table()?;
1100 for (k, v) in headers {
1101 headers_table.set(k.as_str(), v.as_str())?;
1102 }
1103 req_table.set("headers", headers_table)?;
1104
1105 handler.call_async::<Table>(req_table).await
1106}
1107
1108#[cfg(feature = "server")]
1109fn lua_response_to_http(
1110 lua: &Lua,
1111 resp_table: &Table,
1112) -> Result<Response<ServerBody>, hyper::Error> {
1113 let status = resp_table
1114 .get::<Option<u16>>("status")
1115 .unwrap_or(None)
1116 .unwrap_or(200);
1117
1118 if let Ok(Some(sse_fn)) = resp_table.get::<Option<mlua::Function>>("sse") {
1120 let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(32);
1121
1122 let mut builder =
1123 Response::builder().status(StatusCode::from_u16(status).unwrap_or(StatusCode::OK));
1124
1125 if let Ok(Some(headers_table)) = resp_table.get::<Option<Table>>("headers") {
1127 for pair in headers_table.pairs::<String, Value>().flatten() {
1128 let (k, v) = pair;
1129 match v {
1130 Value::String(s) => {
1131 if let Ok(s) = s.to_str() {
1132 builder = builder.header(&k, s.as_ref());
1133 }
1134 }
1135 Value::Table(t) => {
1136 for val in t.sequence_values::<String>().flatten() {
1139 builder = builder.header(&k, val);
1140 }
1141 }
1142 _ => {}
1143 }
1144 }
1145 }
1146
1147 let mut response = builder.body(axum::body::Body::new(SseBody { rx })).unwrap();
1148 let response_headers = response.headers_mut();
1149 if !response_headers.contains_key(hyper::header::CONTENT_TYPE) {
1150 response_headers.insert(
1151 hyper::header::CONTENT_TYPE,
1152 hyper::header::HeaderValue::from_static("text/event-stream"),
1153 );
1154 }
1155 if !response_headers.contains_key(hyper::header::CACHE_CONTROL) {
1156 response_headers.insert(
1157 hyper::header::CACHE_CONTROL,
1158 hyper::header::HeaderValue::from_static("no-cache"),
1159 );
1160 }
1161 if !response_headers.contains_key(hyper::header::CONNECTION) {
1162 response_headers.insert(
1163 hyper::header::CONNECTION,
1164 hyper::header::HeaderValue::from_static("keep-alive"),
1165 );
1166 }
1167
1168 let lua_clone = lua.clone();
1174 tokio::task::spawn_local(async move {
1175 let tx_holder: Rc<RefCell<Option<tokio::sync::mpsc::Sender<Bytes>>>> =
1176 Rc::new(RefCell::new(Some(tx)));
1177 let tx_for_fn = tx_holder.clone();
1178
1179 let send_fn = match lua_clone.create_async_function(move |_lua, event_table: Table| {
1180 let tx_ref = tx_for_fn.clone();
1181 async move {
1182 let formatted = format_sse_event(&event_table)?;
1183 let tx = tx_ref
1184 .borrow()
1185 .clone()
1186 .ok_or_else(|| mlua::Error::runtime("SSE stream closed"))?;
1187 if tx.send(Bytes::from(formatted)).await.is_err() {
1188 return Err(mlua::Error::runtime("SSE stream closed"));
1189 }
1190 Ok(())
1191 }
1192 }) {
1193 Ok(f) => f,
1194 Err(e) => {
1195 error!("http.serve SSE: failed to create send callback: {e}");
1196 return;
1197 }
1198 };
1199
1200 if let Err(e) = sse_fn.call_async::<()>(send_fn).await
1201 && !e.to_string().contains("SSE stream closed")
1202 {
1203 error!("http.serve SSE: handler error: {e}");
1204 }
1205
1206 tx_holder.borrow_mut().take();
1208 });
1209
1210 return Ok(response);
1211 }
1212
1213 let mut builder =
1214 Response::builder().status(StatusCode::from_u16(status).unwrap_or(StatusCode::OK));
1215
1216 let has_content_type =
1217 if let Ok(Some(headers_table)) = resp_table.get::<Option<Table>>("headers") {
1218 let mut found_ct = false;
1219 for pair in headers_table.pairs::<String, Value>().flatten() {
1220 let (k, v) = pair;
1221 if k.eq_ignore_ascii_case("content-type") {
1222 found_ct = true;
1223 }
1224 match v {
1225 Value::String(s) => {
1226 if let Ok(s) = s.to_str() {
1227 builder = builder.header(&k, s.as_ref());
1228 }
1229 }
1230 Value::Table(t) => {
1231 for val in t.sequence_values::<String>().flatten() {
1234 builder = builder.header(&k, val);
1235 }
1236 }
1237 _ => {}
1238 }
1239 }
1240 found_ct
1241 } else {
1242 false
1243 };
1244
1245 let body_bytes = if let Ok(Some(json_table)) = resp_table.get::<Option<Table>>("json") {
1246 let json_val =
1247 lua_value_to_json(&Value::Table(json_table)).unwrap_or(serde_json::Value::Null);
1248 let serialized = serde_json::to_string(&json_val).unwrap_or_else(|_| "null".to_string());
1249 if !has_content_type {
1250 builder = builder.header("content-type", "application/json");
1251 }
1252 Bytes::from(serialized)
1253 } else if let Ok(Some(body_lua)) = resp_table.get::<Option<mlua::String>>("body") {
1254 if !has_content_type {
1255 builder = builder.header("content-type", "text/plain");
1256 }
1257 Bytes::from(body_lua.as_bytes().to_vec())
1258 } else {
1259 if !has_content_type {
1260 builder = builder.header("content-type", "text/plain");
1261 }
1262 Bytes::new()
1263 };
1264
1265 Ok(builder
1266 .body(axum::body::Body::new(Full::new(body_bytes)))
1267 .unwrap())
1268}
1269
1270#[cfg(all(test, feature = "server"))]
1273mod tests {
1274 use super::*;
1275 use axum::Router;
1276 use axum::routing::get;
1277 use mlua::Lua;
1278
1279 #[test]
1285 fn lua_axum_router_round_trips_through_mlua_globals() {
1286 let lua = Lua::new();
1287 let router = Router::new().route("/ping", get(|| async { "pong" }));
1288 let wrapped = LuaAxumRouter(router);
1289
1290 let ud = lua
1291 .create_userdata(wrapped)
1292 .expect("create_userdata for LuaAxumRouter");
1293 lua.globals()
1294 .set("EXTRA_ROUTER", ud)
1295 .expect("stash userdata in globals");
1296
1297 let value: mlua::Value = lua
1298 .globals()
1299 .get("EXTRA_ROUTER")
1300 .expect("read userdata back from globals");
1301 let ud = match value {
1302 mlua::Value::UserData(u) => u,
1303 other => panic!("expected UserData, got {other:?}"),
1304 };
1305 let _borrowed = ud
1306 .borrow::<LuaAxumRouter>()
1307 .expect("downcast to LuaAxumRouter");
1308 }
1309
1310 #[test]
1315 fn lua_axum_router_is_clone_and_preserves_routes() {
1316 let router = Router::<()>::new().route("/health", get(|| async { "ok" }));
1317 let wrapped = LuaAxumRouter(router);
1318 let _cloned = wrapped.clone();
1319 }
1321}