1pub mod middleware;
18pub mod openapi;
19
20#[cfg(test)]
21mod httpapi_test;
22
23use std::collections::HashSet;
24use std::sync::Arc;
25
26use axum::extract::Request;
27use axum::http::{Method, StatusCode, header};
28use axum::response::{IntoResponse, Response};
29use axum::routing::{Router, delete, get, head, options, patch, post, put};
30use serde_json::{Map, Value};
31
32use crate::config::Config;
33use crate::ctx::Ctx;
34use crate::errors;
35use crate::registry::Registry;
36use crate::spec::entry::Entry;
37
38pub const MAX_BODY_BYTES: usize = 1 << 20;
40
41pub fn router(reg: &Registry, ctx: Arc<Ctx>) -> errors::Result<Router> {
45 router_with(
46 reg,
47 ctx,
48 std::collections::HashMap::new(),
49 false,
50 false,
51 &crate::cli::bin_name(),
52 crate::version::version(),
53 )
54}
55
56pub(crate) fn router_with(
58 reg: &Registry,
59 ctx: Arc<Ctx>,
60 defaults: std::collections::HashMap<String, String>,
61 no_server_headers: bool,
62 allow_unknown_query: bool,
63 app_name: &str,
64 app_version: &str,
65) -> errors::Result<Router> {
66 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
69 router_inner(
70 reg,
71 ctx,
72 defaults,
73 no_server_headers,
74 allow_unknown_query,
75 app_name,
76 app_version,
77 )
78 }))
79 .unwrap_or_else(|p| {
80 let msg = p
81 .downcast_ref::<&str>()
82 .map(|s| s.to_string())
83 .or_else(|| p.downcast_ref::<String>().cloned())
84 .unwrap_or_else(|| "route registration panic".to_string());
85 Err(errors::Error::new(
86 errors::Kind::Internal,
87 format!("httpapi: route conflicts with an existing route ({msg})"),
88 ))
89 })
90}
91
92fn router_inner(
93 reg: &Registry,
94 ctx: Arc<Ctx>,
95 defaults: std::collections::HashMap<String, String>,
96 no_server_headers: bool,
97 allow_unknown_query: bool,
98 app_name: &str,
99 app_version: &str,
100) -> errors::Result<Router> {
101 let mut r: Router = Router::new();
102 let mut seen: HashSet<(String, String)> = HashSet::new();
103 for e in reg.all() {
104 if e.http.skip || e.cli.daemon {
105 continue; }
107 if e.http.path.is_empty() {
108 continue; }
110 for method in http_methods(&e) {
113 if !seen.insert((method.clone(), e.http.path.clone())) {
114 return Err(errors::Error::new(
115 errors::Kind::Internal,
116 format!(
117 "httpapi: route {:?} {:?} conflicts with an existing route",
118 method, e.http.path
119 ),
120 ));
121 }
122 let h = handle_entry_c(
123 e.clone(),
124 Arc::clone(&ctx),
125 Arc::new(defaults.clone()),
126 no_server_headers,
127 allow_unknown_query,
128 );
129 let mr = method_router(&method, h)?;
130 r = r.route(&e.http.path, mr);
131 }
132 }
133 r = r
134 .route("/healthz", get(healthz))
135 .route(
136 "/openapi.json",
137 get(openapi::openapi_handler(reg, app_name, app_version)),
138 )
139 .fallback(missing_route);
142 Ok(r)
143}
144
145pub(crate) fn http_methods(e: &crate::spec::Entry) -> Vec<String> {
148 let m = e.http.method.trim();
149 if m.is_empty() {
150 return vec!["GET".to_string(), "POST".to_string()];
151 }
152 m.split(',')
154 .map(|s| s.trim().to_uppercase())
155 .filter(|s| !s.is_empty())
156 .collect()
157}
158
159async fn missing_route(req: Request) -> Response {
161 let lang = req
162 .headers()
163 .get(header::ACCEPT_LANGUAGE)
164 .and_then(|v| v.to_str().ok())
165 .and_then(crate::lang::parse_accept_language);
166 let msg = match lang {
167 Some(l) => crate::lang::t_lang(l, "http.err_not_found"),
168 None => crate::lang::t("http.err_not_found"),
169 };
170 write_error(StatusCode::NOT_FOUND, &msg)
171}
172
173fn method_router<H, T>(method: &str, h: H) -> errors::Result<axum::routing::MethodRouter>
174where
175 H: axum::handler::Handler<T, ()>,
176 T: 'static,
177{
178 Ok(match method {
181 "GET" => get(h),
182 "POST" => post(h),
183 "PUT" => put(h),
184 "PATCH" => patch(h),
185 "DELETE" => delete(h),
186 "HEAD" => head(h),
187 "OPTIONS" => options(h),
188 other => {
189 return Err(errors::Error::new(
190 errors::Kind::Internal,
191 format!("httpapi: unsupported HTTP method {other:?}"),
192 ));
193 }
194 })
195}
196
197#[derive(Clone)]
203pub struct EntryHandler {
204 pub(crate) entry: Arc<Entry>,
205 pub(crate) ctx: Arc<Ctx>,
206 pub(crate) defaults: Arc<std::collections::HashMap<String, String>>,
207 pub(crate) no_server_headers: bool,
209 pub(crate) allow_unknown_query: bool,
211}
212
213impl axum::handler::Handler<(), ()> for EntryHandler {
214 type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Response> + Send>>;
215 fn call(self, req: Request, _state: ()) -> Self::Future {
216 Box::pin(async move {
217 handle_request(
218 &self.entry,
219 &self.ctx,
220 &self.defaults,
221 self.no_server_headers,
222 self.allow_unknown_query,
223 req,
224 )
225 .await
226 })
227 }
228}
229
230pub fn handler_for(entry: Arc<Entry>) -> EntryHandler {
231 handle_entry_c(
232 entry,
233 Arc::new(Ctx::new()),
234 Arc::new(std::collections::HashMap::new()),
235 false,
236 false,
237 )
238}
239
240fn handle_entry_c(
242 entry: Arc<Entry>,
243 ctx: Arc<Ctx>,
244 defaults: Arc<std::collections::HashMap<String, String>>,
245 no_server_headers: bool,
246 allow_unknown_query: bool,
247) -> EntryHandler {
248 EntryHandler {
249 entry,
250 ctx,
251 defaults,
252 no_server_headers,
253 allow_unknown_query,
254 }
255}
256
257async fn handle_request(
258 entry: &Arc<Entry>,
259 ctx: &Arc<Ctx>,
260 defaults: &std::collections::HashMap<String, String>,
261 no_server_headers: bool,
262 allow_unknown_query: bool,
263 req: Request,
264) -> Response {
265 let (parts, body) = req.into_parts();
266 let method = parts.method.clone();
267 let path = parts.uri.path().to_string();
268 let query = parts.uri.query().unwrap_or_default().to_string();
269 let headers = parts.headers.clone();
270
271 let req_lang = headers
275 .get(header::ACCEPT_LANGUAGE)
276 .and_then(|v| v.to_str().ok())
277 .and_then(crate::lang::parse_accept_language);
278 let localize = |key: &str| match req_lang {
279 Some(l) => crate::lang::t_lang(l, key),
280 None => crate::lang::t(key),
281 };
282 let localize_tf = |key: &str, params: &[&str]| match req_lang {
283 Some(l) => crate::lang::tf_lang(l, key, params),
284 None => crate::lang::tf(key, params),
285 };
286
287 if !allow_unknown_query && !query.is_empty() {
291 let mut allowed: Vec<&str> = Vec::new();
292 for f in &entry.root.children {
293 if f.skip || !matches!(f.http.location.as_str(), "" | "query") {
294 continue;
295 }
296 let n = http_name(f);
297 if !allowed.contains(&n) {
298 allowed.push(n);
299 }
300 }
301 let names = if allowed.is_empty() {
302 "—".to_string()
303 } else {
304 allowed.join(", ")
305 };
306 for (k, _) in parse_form_query(&query) {
307 if !allowed.contains(&k.as_str()) {
308 let msg = localize_tf("http.err_unknown_query", &[&k, &names]);
309 return write_coded_error(&errors::Error::new(errors::Kind::InvalidInput, msg));
310 }
311 }
312 }
313
314 let mut m: Map<String, Value> = Map::new();
315 for (k, v) in entry.http_defaults() {
317 m.insert(k, v);
318 }
319 for (k, v) in defaults.iter() {
321 if !m.contains_key(k) {
322 m.insert(k.clone(), Value::String(v.clone()));
323 }
324 }
325 let body_bytes: Vec<u8> = if method != Method::GET && method != Method::HEAD {
327 collect_body(body, MAX_BODY_BYTES).await
328 } else {
329 Vec::new()
330 };
331 if !body_bytes.is_empty() {
332 let json_declared = headers
333 .get(header::CONTENT_TYPE)
334 .and_then(|v| v.to_str().ok())
335 .map(|v| v.contains("application/json"))
336 .unwrap_or(false);
337 match serde_json::from_slice::<serde_json::Value>(&body_bytes) {
338 Ok(Value::Object(parsed)) => {
339 for (k, v) in parsed {
340 m.insert(k, v);
341 }
342 }
343 Ok(_) => {
344 return write_error(StatusCode::BAD_REQUEST, &localize("http.err_invalid_json"));
345 }
346 Err(_) if json_declared => {
347 return write_error(StatusCode::BAD_REQUEST, &localize("http.err_invalid_json"));
349 }
350 Err(_) => {
351 }
353 }
354 }
355 let mut form_parsed: Option<Vec<(String, String)>> = None;
357
358 for f in &entry.root.children {
359 if f.skip {
360 if f.http.location == "header"
362 && let Some(v) = headers.get(http_name(f)).and_then(|v| v.to_str().ok())
363 && !v.is_empty()
364 {
365 m.insert(f.name.clone(), Value::String(v.to_string()));
366 }
367 continue;
368 }
369 match f.http.location.as_str() {
370 "" | "query" => {
371 if let Some(vs) = query_values(&query, http_name(f)) {
373 if is_string_slice(f) {
374 m.insert(
375 f.json_name.clone(),
376 Value::Array(vs.iter().map(|s| Value::String(s.clone())).collect()),
377 );
378 } else if let Some(first) = vs.first() {
379 m.insert(f.json_name.clone(), Value::String(first.clone()));
380 }
381 }
382 }
383 "header" => {
384 if let Some(v) = headers.get(http_name(f)).and_then(|v| v.to_str().ok())
385 && !v.is_empty()
386 {
387 m.insert(f.json_name.clone(), Value::String(v.to_string()));
388 }
389 }
390 "path" => {
391 if let Some(v) = path_param(&entry.http.path, &path, http_name(f)) {
392 m.insert(f.json_name.clone(), Value::String(v));
393 }
394 }
395 "form" => {
396 if form_parsed.is_none() {
397 form_parsed = Some(crate::httpapi::parse_form_query(&String::from_utf8_lossy(
399 &body_bytes,
400 )));
401 }
402 if let Some((_, v)) = form_parsed
403 .as_ref()
404 .unwrap()
405 .iter()
406 .find(|(k, _)| k == http_name(f))
407 {
408 m.insert(f.json_name.clone(), Value::String(v.clone()));
409 }
410 }
411 _ => {}
412 }
413 }
414
415 let per_req_ctx = match req_lang {
416 Some(l) => Arc::new(ctx.with_language(l.as_str())),
417 None => Arc::clone(ctx),
418 };
419 let started = std::time::Instant::now();
420 let mut resp = match (entry.invoke)(&per_req_ctx, &m) {
421 Ok(out) => {
422 let s = serde_json::to_string_pretty(&out).unwrap_or_else(|_| "null".to_string());
423 json_response(StatusCode::OK, &s)
424 }
425 Err(e) => write_coded_error(&e),
426 };
427 if !no_server_headers {
429 let h = resp.headers_mut();
430 if let Ok(v) = header::HeaderValue::from_str(&entry.name) {
431 let _ = h.insert(header::HeaderName::from_static("x-xyz-command"), v);
432 }
433 let dur = started.elapsed().as_millis().to_string();
434 if let Ok(v) = header::HeaderValue::from_str(&dur) {
435 let _ = h.insert(header::HeaderName::from_static("x-xyz-duration-ms"), v);
436 }
437 }
438 resp
439}
440
441pub(crate) fn http_name(f: &crate::spec::FieldMeta) -> &str {
443 if !f.http.name.is_empty() {
444 return f.http.name.as_str();
445 }
446 if !f.json_name.is_empty() {
447 return f.json_name.as_str();
448 }
449 f.name.as_str()
450}
451
452fn is_string_slice(f: &crate::spec::FieldMeta) -> bool {
453 matches!(f.kind, crate::spec::FieldKind::Slice)
454}
455
456pub(crate) fn parse_form_query(s: &str) -> Vec<(String, String)> {
459 s.split('&')
460 .filter(|p| !p.is_empty())
461 .map(|pair| match pair.split_once('=') {
462 Some((k, v)) => (pct_decode(k), pct_decode(v)),
463 None => (pct_decode(pair), String::new()),
464 })
465 .collect()
466}
467
468fn query_values(query: &str, key: &str) -> Option<Vec<String>> {
469 let pairs = parse_form_query(query);
470 let hit: Vec<String> = pairs
471 .iter()
472 .filter(|(k, _)| k == key)
473 .map(|(_, v)| v.clone())
474 .collect();
475 if hit.is_empty() { None } else { Some(hit) }
476}
477
478fn path_param(template: &str, path: &str, name: &str) -> Option<String> {
480 let tsegs: Vec<&str> = template.split('/').collect();
481 let psegs: Vec<&str> = path.split('/').collect();
482 if tsegs.len() != psegs.len() {
483 return None;
484 }
485 for (t, p) in tsegs.iter().zip(psegs.iter()) {
486 if let Some(inner) = t.strip_prefix('{').and_then(|s| s.strip_suffix('}'))
487 && inner == name
488 {
489 return Some(pct_decode(p));
490 }
491 }
492 None
493}
494
495fn pct_decode(s: &str) -> String {
496 let bytes = s.as_bytes();
497 let mut out = Vec::with_capacity(bytes.len());
498 let mut i = 0;
499 while i < bytes.len() {
500 if bytes[i] == b'%' && i + 2 < bytes.len() + 1 {
501 let h = hex_val(bytes.get(i + 1));
502 let l = hex_val(bytes.get(i + 2));
503 if let (Some(h), Some(l)) = (h, l) {
504 out.push(h * 16 + l);
505 i += 3;
506 continue;
507 }
508 }
509 if bytes[i] == b'+' {
510 out.push(b' ');
511 } else {
512 out.push(bytes[i]);
513 }
514 i += 1;
515 }
516 String::from_utf8_lossy(&out).into_owned()
517}
518
519fn hex_val(b: Option<&u8>) -> Option<u8> {
520 match b.copied()? {
521 b'0'..=b'9' => Some(b.unwrap() - b'0'),
522 b'a'..=b'f' => Some(b.unwrap() - b'a' + 10),
523 b'A'..=b'F' => Some(b.unwrap() - b'A' + 10),
524 _ => None,
525 }
526}
527
528fn write_coded_error(e: &errors::Error) -> Response {
531 let status =
532 StatusCode::from_u16(errors::error_status(e)).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
533 let mut body = errors::error_body(e);
534 if body.error.is_empty() {
535 body.error = status.canonical_reason().unwrap_or("error").to_string();
536 }
537 let s = serde_json::to_string(&body).unwrap_or_else(|_| "{\"error\":\"\"}".to_string());
538 json_response(status, &s)
539}
540
541fn write_error(status: StatusCode, msg: &str) -> Response {
542 let msg = if msg.is_empty() {
543 status.canonical_reason().unwrap_or("error").to_string()
544 } else {
545 msg.to_string()
546 };
547 let body = serde_json::to_string(&serde_json::json!({ "error": msg }))
549 .unwrap_or_else(|_| "{\"error\":\"\"}".to_string());
550 json_response(status, &body)
551}
552
553fn json_response(status: StatusCode, body: &str) -> Response {
554 let mut b = body.to_string();
555 b.push('\n');
556 (
557 status,
558 [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
559 b,
560 )
561 .into_response()
562}
563
564async fn healthz() -> Response {
565 json_response(StatusCode::OK, "{\"status\":\"ok\"}")
566}
567
568async fn collect_body(body: axum::body::Body, cap: usize) -> Vec<u8> {
570 use futures_util::StreamExt as _;
571 let mut out = Vec::new();
572 let mut stream = body.into_data_stream();
573 while let Some(Ok(chunk)) = stream.next().await {
574 let rem = cap.saturating_sub(out.len());
575 if rem == 0 {
576 break;
577 }
578 out.extend_from_slice(&chunk[..chunk.len().min(rem)]);
579 }
580 out
581}
582
583pub(crate) fn normalize_addr(addr: &str) -> String {
587 if let Some(rest) = addr.strip_prefix(':')
588 && !rest.contains(':')
589 {
590 return format!("0.0.0.0:{rest}");
591 }
592 addr.to_string()
593}
594
595pub(crate) fn serve(
596 ctx: &Ctx,
597 reg: &Registry,
598 args: &[String],
599 cfg: Config,
600 mount_mcp: bool,
601) -> i32 {
602 let cfg = crate::builtins::parse_serve_args(args, cfg);
603 let sctx = Arc::new(ctx.clone());
604 let mut router = match router_with(
605 reg,
606 Arc::clone(&sctx),
607 cfg.channel_defaults.clone(),
608 cfg.no_server_headers,
609 cfg.allow_unknown_query,
610 &cfg.resolved_name(),
611 &cfg.resolved_version(),
612 ) {
613 Ok(r) => r,
614 Err(e) => {
615 crate::logx::errorf(format_args!("{e}"));
616 return 2;
617 }
618 };
619 let mut mcp_note = String::new();
620 #[cfg(feature = "mcp")]
621 {
622 if mount_mcp
623 && let Some(m) = crate::mcp::mountable(
624 reg,
625 &crate::mcp::Options {
626 name: cfg.resolved_name(),
630 version: cfg.resolved_version(),
631 bearer_tokens: cfg.bearer_tokens.clone(),
632 default_ctx: Some(sctx.clone()),
633 defaults: cfg.channel_defaults.clone(),
634 ..Default::default()
635 },
636 )
637 {
638 router = router.nest_service("/mcp", m);
639 mcp_note = " + /mcp".to_string();
640 }
641 }
642 if !cfg.cors_origins.is_empty() {
643 crate::logx::debugf(format_args!(
644 "{}",
645 crate::lang::tf("log.cors_on", &[&format!("{:?}", cfg.cors_origins)])
646 ));
647 }
648 let router = middleware::apply(
651 router,
652 cfg.bearer_tokens.clone(),
653 cfg.cors_origins.clone(),
654 cfg.timeout,
655 );
656 let router = router.layer(axum::middleware::from_fn(middleware::server_context_mw(
658 cfg.resolved_name(),
659 cfg.resolved_version(),
660 cfg.response_headers.clone(),
661 !cfg.no_server_headers,
662 )));
663
664 let tls_on = !cfg.cert_file.is_empty() || !cfg.key_file.is_empty();
665 if tls_on && (cfg.cert_file.is_empty() || cfg.key_file.is_empty()) {
666 crate::logx::errorf(format_args!("TLS requires both --tls-cert and --tls-key"));
667 return 2;
668 }
669 let scheme = if tls_on { "https" } else { "http" };
670 crate::logx::infof(format_args!(
671 "{}",
672 crate::lang::tf("log.serve_listening", &[scheme, &cfg.addr, &mcp_note])
673 ));
674
675 let rt = match tokio::runtime::Builder::new_multi_thread()
676 .enable_all()
677 .build()
678 {
679 Ok(rt) => rt,
680 Err(e) => {
681 crate::logx::errorf(format_args!("{e}"));
682 return 1;
683 }
684 };
685 rt.block_on(async move {
686 let listener =
687 match tokio::net::TcpListener::bind(&crate::httpapi::normalize_addr(&cfg.addr)).await {
688 Ok(l) => l,
689 Err(e) => {
690 crate::logx::errorf(format_args!("{e}"));
691 return 1;
692 }
693 };
694 let handle = axum_server::Handle::new();
695 let cert_file = cfg.cert_file.clone();
696 let key_file = cfg.key_file.clone();
697 let serve = {
698 let router = router.clone();
699 let handle = handle.clone();
700 move || async move {
701 if tls_on {
702 let tls_config = match load_rustls_config(&cert_file, &key_file) {
703 Ok(c) => c,
704 Err(e) => {
705 crate::logx::errorf(format_args!("{e}"));
706 return 1;
707 }
708 };
709 let rustls_cfg = axum_server::tls_rustls::RustlsConfig::from_config(
710 std::sync::Arc::new(tls_config),
711 );
712 let server = match axum_server::from_tcp_rustls(
713 listener.into_std().unwrap(),
714 rustls_cfg,
715 ) {
716 Ok(s) => s,
717 Err(e) => {
718 crate::logx::errorf(format_args!("{e}"));
719 return 1;
720 }
721 };
722 let _ = server
723 .handle(handle.clone())
724 .serve(router.clone().into_make_service())
725 .await;
726 } else {
727 let server = match axum_server::from_tcp(listener.into_std().unwrap()) {
728 Ok(s) => s,
729 Err(e) => {
730 crate::logx::errorf(format_args!("{e}"));
731 return 1;
732 }
733 };
734 let _ = server
735 .handle(handle.clone())
736 .serve(router.clone().into_make_service())
737 .await;
738 };
739 0
740 }
741 };
742 let _ = &serve;
743 let server_handle = handle.clone();
744 let serve_fut = tokio::spawn(async move { serve().await });
745 let cancel_fut = async move {
746 sctx.cancelled_async().await;
747 server_handle.graceful_shutdown(Some(std::time::Duration::from_secs(5)));
748 };
749 tokio::select! {
750 join_res = serve_fut => join_res.unwrap_or(1),
751 _ = cancel_fut => {
752 crate::logx::infof(format_args!("{}", crate::lang::t("log.graceful")));
753 0
754 }
755 }
756 })
757}
758
759#[cfg(feature = "http-stack")]
760fn load_rustls_config(cert_file: &str, key_file: &str) -> errors::Result<rustls::ServerConfig> {
761 use std::io::BufReader;
762 let certs = rustls_pemfile::certs(&mut BufReader::new(
763 std::fs::File::open(cert_file).map_err(errors::Error::from)?,
764 ))
765 .collect::<Result<Vec<_>, _>>()
766 .map_err(|e| errors::Error::new(errors::Kind::Internal, format!("cert parse: {e}")))?;
767 let key = rustls_pemfile::private_key(&mut BufReader::new(
768 std::fs::File::open(key_file).map_err(errors::Error::from)?,
769 ))
770 .map_err(|e| errors::Error::new(errors::Kind::Internal, format!("key parse: {e}")))?
771 .ok_or_else(|| {
772 errors::Error::new(errors::Kind::Internal, "no private key found".to_string())
773 })?;
774 let _ = rustls::crypto::ring::default_provider().install_default();
777 let config = rustls::ServerConfig::builder()
778 .with_no_client_auth()
779 .with_single_cert(certs, key)
780 .map_err(|e| errors::Error::new(errors::Kind::Internal, format!("tls config: {e}")))?;
781 Ok(config)
782}