1use std::collections::{HashMap, VecDeque};
40use std::net::SocketAddr;
41use std::pin::Pin;
42use std::sync::{Arc, Mutex, OnceLock};
43use std::task::{Context, Poll};
44use std::time::{Duration, Instant};
45
46use async_trait::async_trait;
47use http_body_util::combinators::BoxBody;
48use http_body_util::{BodyExt, Full};
49use hyper::body::{Bytes, Frame};
50use hyper::service::service_fn;
51use hyper::{Request, Response, StatusCode};
52use hyper_util::rt::TokioIo;
53use tokio::sync::{mpsc, oneshot};
54
55use crate::envelope::EventEnvelope;
56use crate::event_stream;
57use crate::function::{AppError, ComposableFunction};
58use crate::platform::Platform;
59use crate::post_office::PostOffice;
60use crate::trace;
61use crate::util::app_config_reader::AppConfigReader;
62use crate::util::config_reader::ConfigReader;
63use crate::util::w3c_trace;
64
65use super::routing::{AssignedRoute, RouteInfo, RoutingTable};
66
67pub const MY_CORRELATION_ID: &str = "my_correlation_id";
70
71pub const ASYNC_HTTP_RESPONSE: &str = "async.http.response";
74
75pub const ASYNC_HTTP_RESPONSE_STREAM_POOL: &str = "async.http.response.stream";
81
82const RESPONSE_HANDLER_INSTANCES: usize = 500;
85
86const STREAM_EVENT_BUFFER: usize = 64;
88const STREAM_FRAME_BUFFER: usize = 64;
90
91type HttpBody = BoxBody<Bytes, std::convert::Infallible>;
94
95fn full(bytes: Bytes) -> HttpBody {
97 BoxBody::new(Full::new(bytes))
98}
99
100fn lane_pool() -> &'static Mutex<VecDeque<String>> {
107 static POOL: OnceLock<Mutex<VecDeque<String>>> = OnceLock::new();
108 POOL.get_or_init(|| Mutex::new(VecDeque::new()))
109}
110
111pub fn checkout_lane() -> Option<String> {
114 lane_pool().lock().expect("lane pool poisoned").pop_front()
115}
116
117pub fn release_lane(route: String) {
120 lane_pool()
121 .lock()
122 .expect("lane pool poisoned")
123 .push_back(route);
124}
125
126pub fn available_lanes() -> usize {
128 lane_pool().lock().expect("lane pool poisoned").len()
129}
130
131struct StreamContext {
135 sender: mpsc::Sender<EventEnvelope>,
136 data_frames: u64,
137}
138
139fn pending_streams() -> &'static Mutex<HashMap<String, StreamContext>> {
143 static PENDING: OnceLock<Mutex<HashMap<String, StreamContext>>> = OnceLock::new();
144 PENDING.get_or_init(|| Mutex::new(HashMap::new()))
145}
146
147struct EdgeTrace {
152 platform: Platform,
153 trace_id: String,
154 trace_path: String,
155 span_id: String,
156 parent_span: Option<String>,
157 start: String,
158 started: Instant,
159}
160
161impl EdgeTrace {
162 fn new(
163 platform: &Platform,
164 trace_id: &str,
165 trace_path: &str,
166 span_id: &str,
167 parent_span: &Option<String>,
168 ) -> Self {
169 EdgeTrace {
170 platform: platform.clone(),
171 trace_id: trace_id.to_string(),
172 trace_path: trace_path.to_string(),
173 span_id: span_id.to_string(),
174 parent_span: parent_span.clone(),
175 start: trace::iso8601_utc_now(),
176 started: Instant::now(),
177 }
178 }
179
180 async fn record(self, status: i32, error: Option<String>) {
187 if !self
188 .platform
189 .has_route(crate::telemetry::DISTRIBUTED_TRACING)
190 {
191 return; }
193 let elapsed_ms = self.started.elapsed().as_secs_f64() * 1000.0;
194 let mut metrics = serde_json::Map::new();
195 let mut put = |k: &str, v: serde_json::Value| {
196 metrics.insert(k.to_string(), v);
197 };
198 put(
199 "origin",
200 serde_json::Value::String(Platform::origin().to_string()),
201 );
202 put("id", serde_json::Value::String(self.trace_id.clone()));
203 put(
204 "service",
205 serde_json::Value::String("http.request".to_string()),
206 );
207 put("path", serde_json::Value::String(self.trace_path.clone()));
208 put("start", serde_json::Value::String(self.start.clone()));
209 put(
210 "exec_time",
211 serde_json::Value::from((elapsed_ms * 1000.0).round() / 1000.0),
212 );
213 put("status", serde_json::Value::from(status));
214 if status >= 400 {
215 put("success", serde_json::Value::Bool(false));
216 put(
217 "exception",
218 serde_json::Value::String(error.unwrap_or_else(|| format!("status={status}"))),
219 );
220 } else {
221 put("success", serde_json::Value::Bool(true));
222 }
223 put("span_id", serde_json::Value::String(self.span_id.clone()));
224 if let Some(parent) = &self.parent_span {
225 put("parent_span_id", serde_json::Value::String(parent.clone()));
226 }
227 let mut dataset = serde_json::Map::new();
228 dataset.insert("trace".to_string(), serde_json::Value::Object(metrics));
229 match EventEnvelope::new()
230 .set_to(crate::telemetry::DISTRIBUTED_TRACING)
231 .set_body(serde_json::Value::Object(dataset))
232 {
233 Ok(event) => {
234 let _ = self
235 .platform
236 .deliver(crate::telemetry::DISTRIBUTED_TRACING, event)
237 .await;
238 }
239 Err(e) => log::error!("Unable to send to distributed.tracing - {e}"),
240 }
241 }
242}
243
244fn cleanup_stream(context_id: &str, lane: &str) {
247 let removed = pending_streams()
248 .lock()
249 .expect("pending streams poisoned")
250 .remove(context_id);
251 if removed.is_some() {
252 release_lane(lane.to_string());
253 }
254}
255
256pub struct StreamLaneService;
262
263#[async_trait]
264impl ComposableFunction for StreamLaneService {
265 async fn handle_event(
266 &self,
267 _headers: HashMap<String, String>,
268 input: EventEnvelope,
269 _instance: usize,
270 ) -> Result<EventEnvelope, AppError> {
271 if let Some(context_id) = input.correlation_id().map(str::to_string) {
272 let marker = stream_marker(&input).ok().flatten();
273 let sender = {
274 let mut pending = pending_streams().lock().expect("pending streams poisoned");
275 match pending.get_mut(&context_id) {
276 Some(context) => {
277 match marker {
278 Some(event_stream::DATA) => context.data_frames += 1,
279 Some(_) => {
280 let frames = context.data_frames.to_string();
284 trace::with_current_mut(|state| {
285 state.annotations.insert(
286 "frames".to_string(),
287 serde_json::Value::String(frames),
288 );
289 });
290 }
291 None => {}
292 }
293 Some(context.sender.clone())
294 }
295 None => None,
296 }
297 };
298 if let Some(sender) = sender {
299 let _ = sender.send(input).await;
302 }
303 }
304 Ok(EventEnvelope::new())
305 }
306}
307
308struct ChannelBody {
312 rx: mpsc::Receiver<Frame<Bytes>>,
313}
314
315impl hyper::body::Body for ChannelBody {
316 type Data = Bytes;
317 type Error = std::convert::Infallible;
318
319 fn poll_frame(
320 mut self: Pin<&mut Self>,
321 cx: &mut Context<'_>,
322 ) -> Poll<Option<Result<Frame<Bytes>, Self::Error>>> {
323 self.rx.poll_recv(cx).map(|frame| frame.map(Ok))
324 }
325}
326
327fn keep_alive_ms() -> u64 {
330 static KEEP_ALIVE: OnceLock<u64> = OnceLock::new();
331 *KEEP_ALIVE.get_or_init(|| {
332 let config = AppConfigReader::get_instance();
333 let text = config.get_property_or("event.stream.keep.alive", "30s");
334 let trimmed = text.trim().to_lowercase();
335 if trimmed == "0" || trimmed == "0s" || trimmed == "0ms" || trimmed == "0m" {
336 0
337 } else {
338 super::routing::parse_timeout(Some(&trimmed)).as_millis() as u64
339 }
340 })
341}
342
343const PROTECTED_METADATA: [&str; 5] = [
346 "my_route",
347 "my_trace_id",
348 "my_trace_path",
349 MY_CORRELATION_ID,
350 "x-event-api",
351];
352
353fn pending_responses() -> &'static Mutex<HashMap<String, oneshot::Sender<EventEnvelope>>> {
357 static PENDING: OnceLock<Mutex<HashMap<String, oneshot::Sender<EventEnvelope>>>> =
358 OnceLock::new();
359 PENDING.get_or_init(|| Mutex::new(HashMap::new()))
360}
361
362pub struct AsyncHttpResponseService;
371
372#[async_trait]
373impl ComposableFunction for AsyncHttpResponseService {
374 async fn handle_event(
375 &self,
376 _headers: HashMap<String, String>,
377 input: EventEnvelope,
378 _instance: usize,
379 ) -> Result<EventEnvelope, AppError> {
380 if let Some(context_id) = input.correlation_id().map(str::to_string) {
381 let sender = pending_responses()
382 .lock()
383 .expect("pending http contexts poisoned")
384 .remove(&context_id);
385 if let Some(sender) = sender {
386 let _ = sender.send(input);
387 }
388 }
389 Ok(EventEnvelope::new())
390 }
391}
392
393static SERVER_ADDR: OnceLock<SocketAddr> = OnceLock::new();
398
399pub fn server_address() -> Option<SocketAddr> {
403 SERVER_ADDR.get().copied()
404}
405
406struct RouterState {
407 table: RoutingTable,
408 platform: Platform,
409 trace_header: String,
410 cid_header: String,
411 traceparent_header: String,
420}
421
422pub async fn start_http_server(platform: &Platform) -> Result<SocketAddr, AppError> {
428 let config = AppConfigReader::get_instance();
429 if !platform.has_route(ASYNC_HTTP_RESPONSE) {
433 if let Err(e) = platform.register_private(
434 ASYNC_HTTP_RESPONSE,
435 Arc::new(AsyncHttpResponseService),
436 RESPONSE_HANDLER_INSTANCES,
437 ) {
438 if !platform.has_route(ASYNC_HTTP_RESPONSE) {
439 return Err(e);
440 }
441 }
442 }
443 let members = platform.register_route_pool(
454 ASYNC_HTTP_RESPONSE_STREAM_POOL,
455 Arc::new(StreamLaneService),
456 RESPONSE_HANDLER_INSTANCES,
457 )?;
458 static POOL_FILLED: OnceLock<()> = OnceLock::new();
459 POOL_FILLED.get_or_init(|| {
460 for lane_route in members {
461 release_lane(lane_route);
462 }
463 });
464 let rest_yaml = config.get_property_or("yaml.rest.automation", "classpath:/rest.yaml");
465 let reader = ConfigReader::load(&rest_yaml)
466 .map_err(|e| AppError::new(500, format!("Unable to load {rest_yaml} - {e}")))?;
467 let mut table = RoutingTable::load(&reader)?;
468 merge_default_endpoints(&mut table)?;
469 for (methods, url, service) in table.retain_available(|service| platform.has_route(service)) {
474 log::warn!("Skip {methods} {url} - Service {service} not available");
475 }
476 let table = table;
477 for route in table.routes() {
478 log::info!(
479 "{} {} -> {}",
480 route.methods.join(","),
481 route.url,
482 route.service
483 );
484 }
485 let port: u16 = config
486 .get_property_or("rest.server.port", "8085")
487 .parse()
488 .map_err(|_| AppError::new(500, "Invalid rest.server.port"))?;
489 let state = Arc::new(RouterState {
490 table,
491 platform: platform.clone(),
492 trace_header: config.get_property_or("http.trace.id.header", "X-Trace-Id"),
493 cid_header: config.get_property_or("http.correlation.id.header", "X-Correlation-Id"),
494 traceparent_header: config
495 .get_property_or("http.traceparent.header", w3c_trace::TRACEPARENT),
496 });
497 log::info!("Correlation-id HTTP header is '{}'", state.cid_header);
500 log::info!("Trace-id HTTP header is '{}'", state.trace_header);
501 log::info!("Traceparent HTTP header is '{}'", state.traceparent_header);
502 let listener = tokio::net::TcpListener::bind(("0.0.0.0", port))
503 .await
504 .map_err(|e| AppError::new(500, format!("Unable to bind port {port} - {e}")))?;
505 let addr = listener
506 .local_addr()
507 .map_err(|e| AppError::new(500, e.to_string()))?;
508 let _ = SERVER_ADDR.set(addr);
509 log::info!("REST automation service started on port {}", addr.port());
510 tokio::spawn(async move {
511 loop {
512 let Ok((stream, peer)) = listener.accept().await else {
513 break;
514 };
515 let state = state.clone();
516 tokio::spawn(async move {
517 let io = TokioIo::new(stream);
518 let service = service_fn(move |request| {
519 let state = state.clone();
520 async move { handle(state, request, peer).await }
521 });
522 if let Err(e) = hyper::server::conn::http1::Builder::new()
523 .serve_connection(io, service)
524 .with_upgrades()
525 .await
526 {
527 log::debug!("HTTP connection ended - {e}");
528 }
529 });
530 }
531 });
532 Ok(addr)
533}
534
535async fn handle(
536 state: Arc<RouterState>,
537 request: Request<hyper::body::Incoming>,
538 peer: SocketAddr,
539) -> Result<Response<HttpBody>, hyper::Error> {
540 if super::ws_server::is_ws_upgrade(&request) {
543 return Ok(super::ws_server::handle_ws_upgrade(
544 &state.platform,
545 request,
546 peer.ip().to_string(),
547 )
548 .map(BoxBody::new));
549 }
550 let method = request.method().as_str().to_uppercase();
551 let path = request.uri().path().to_string();
552 let query_text = request.uri().query().unwrap_or("").to_string();
553 let mut headers: HashMap<String, String> = HashMap::new();
555 for (name, value) in request.headers() {
556 if let Ok(value) = value.to_str() {
557 headers.insert(name.as_str().to_lowercase(), value.to_string());
558 }
559 }
560 let body_bytes = match request.into_body().collect().await {
561 Ok(collected) => collected.to_bytes(),
562 Err(_) => Bytes::new(),
563 };
564 let assigned = state.table.find(&method, &path).or_else(|| {
569 if path == "/" {
570 state.table.find(&method, "/index.html")
571 } else {
572 None
573 }
574 });
575 let Some(assigned) = assigned else {
576 if state.table.path_matches_any_method(&path) {
579 return Ok(error_response(405, "Method not allowed"));
580 }
581 if method == "GET" || method == "HEAD" {
585 if let Some(response) =
586 serve_static(&state, &path, &query_text, &headers, peer, method == "HEAD").await
587 {
588 return Ok(response);
589 }
590 }
591 return Ok(error_response(404, "Resource not found"));
592 };
593 if method == "OPTIONS" {
598 let Some(cors) = assigned
599 .info
600 .cors
601 .as_ref()
602 .filter(|c| !c.options.is_empty())
603 else {
604 return Ok(error_response(405, "Method not allowed"));
605 };
606 let mut response = Response::builder().status(StatusCode::NO_CONTENT);
607 for (name, value) in &cors.options {
608 response = response.header(name, value);
609 }
610 return Ok(response.body(full(Bytes::new())).expect("static response"));
611 }
612 let mut edge: Option<EdgeTrace> = None;
613 let (response, error) = match process(
614 &state, assigned, method, path, query_text, headers, body_bytes, peer, &mut edge,
615 )
616 .await
617 {
618 Ok(response) => (response, None),
619 Err(e) => (
620 error_response(e.status(), e.message()),
621 Some(e.message().to_string()),
622 ),
623 };
624 if let Some(edge) = edge {
627 edge.record(response.status().as_u16() as i32, error).await;
628 }
629 Ok(response)
630}
631
632#[allow(clippy::too_many_arguments)]
633async fn process(
634 state: &RouterState,
635 assigned: AssignedRoute<'_>,
636 method: String,
637 path: String,
638 query_text: String,
639 mut headers: HashMap<String, String>,
640 body_bytes: Bytes,
641 peer: SocketAddr,
642 edge: &mut Option<EdgeTrace>,
643) -> Result<Response<HttpBody>, AppError> {
644 let info = assigned.info;
645 if let Some(header_info) = &info.headers {
647 header_info.request.apply(&mut headers);
648 }
649 if let Some(flow) = &info.flow {
652 headers.insert("x-flow-id".to_string(), flow.clone());
653 }
654 let trace_header = info
656 .trace_id_header
657 .as_deref()
658 .unwrap_or(&state.trace_header)
659 .to_lowercase();
660 let cid_header = info
661 .correlation_id_header
662 .as_deref()
663 .unwrap_or(&state.cid_header)
664 .to_lowercase();
665 let traceparent = headers
674 .get(w3c_trace::TRACEPARENT)
675 .and_then(|value| w3c_trace::parse(value))
676 .or_else(|| {
677 let traceparent_header = info
678 .traceparent_header
679 .as_deref()
680 .unwrap_or(&state.traceparent_header)
681 .to_lowercase();
682 if traceparent_header == w3c_trace::TRACEPARENT {
683 None
684 } else {
685 headers
686 .get(&traceparent_header)
687 .and_then(|value| w3c_trace::parse(value))
688 }
689 });
690 let (trace_id, parent_span) = match &traceparent {
691 Some((trace_id, parent)) => (Some(trace_id.clone()), Some(parent.clone())),
692 None => (headers.get(&trace_header).cloned(), None),
693 };
694 let trace_id = if info.tracing {
695 Some(trace_id.unwrap_or_else(trace::new_trace_id))
696 } else {
697 None
698 };
699 let cid = headers.get(&cid_header).cloned().unwrap_or_else(|| {
702 if cid_header == trace_header {
703 trace_id
704 .clone()
705 .unwrap_or_else(|| uuid::Uuid::new_v4().simple().to_string())
706 } else {
707 uuid::Uuid::new_v4().simple().to_string()
708 }
709 });
710 headers.insert(cid_header.clone(), cid.clone());
714 headers
722 .entry("x-ttl".to_string())
723 .or_insert_with(|| (info.timeout.as_secs().max(1) * 1000).to_string());
724 let mut query: HashMap<String, serde_json::Value> = HashMap::new();
729 for pair in query_text.split('&').filter(|p| !p.is_empty()) {
730 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
731 let (name, value) = (url_decode(name), url_decode(value));
732 match query.get_mut(&name) {
733 None => {
734 query.insert(name, serde_json::Value::String(value));
735 }
736 Some(serde_json::Value::Array(values)) => {
737 values.push(serde_json::Value::String(value));
738 }
739 Some(existing) => {
740 let first = existing.clone();
741 *existing = serde_json::Value::Array(vec![first, serde_json::Value::String(value)]);
742 }
743 }
744 }
745 let path_params: HashMap<String, String> = assigned
746 .path_params
747 .iter()
748 .map(|(k, v)| (k.clone(), url_decode(v)))
749 .collect();
750 let cookies: HashMap<String, String> = headers
754 .remove("cookie")
755 .map(|header| {
756 header
757 .split(';')
758 .filter_map(|item| item.split_once('='))
759 .map(|(name, value)| (name.trim().to_string(), value.trim().to_string()))
760 .collect()
761 })
762 .unwrap_or_default();
763 let accept = headers.get("accept").cloned();
767 let parsed = parse_body(&headers, &body_bytes);
768 if let ParsedBody::Form(form) = &parsed {
772 for (name, value) in form {
773 query.insert(name.clone(), serde_json::Value::String(value.clone()));
774 }
775 }
776 let mut http_request = crate::automation::AsyncHttpRequest::new()
783 .set_method(&method)
784 .set_url(&path)
785 .set_remote_ip(&peer.ip().to_string())
786 .set_secure(headers.get("x-forwarded-proto").map(String::as_str) == Some("https"))
789 .set_target_host(&headers.get("host").cloned().unwrap_or_default())
790 .set_route_timeout_seconds(info.timeout.as_secs());
793 for (key, value) in &headers {
794 http_request = http_request.set_header(key, value);
795 }
796 for (key, value) in &path_params {
797 http_request = http_request.set_path_parameter(key, value);
798 }
799 for (key, value) in &query {
800 http_request = match value {
801 serde_json::Value::Array(values) => {
802 let values: Vec<&str> = values
803 .iter()
804 .map(|v| v.as_str().unwrap_or_default())
805 .collect();
806 http_request.set_query_parameter_values(key, &values)
807 }
808 serde_json::Value::String(value) => http_request.set_query_parameter(key, value),
809 other => http_request.set_query_parameter(key, &other.to_string()),
810 };
811 }
812 http_request = match &parsed {
817 ParsedBody::Value(value) => http_request
818 .set_body(rmpv::ext::to_value(value).map_err(|e| AppError::new(500, e.to_string()))?),
819 ParsedBody::Bytes(bytes) => http_request.set_body(rmpv::Value::Binary(bytes.clone())),
820 ParsedBody::Form(_) => http_request.set_body(rmpv::Value::Nil),
821 };
822 if !query_text.is_empty() {
825 http_request = http_request.set_query_string(&query_text);
826 }
827 for (key, value) in &cookies {
828 http_request = http_request.set_cookie(key, value);
829 }
830 let po = PostOffice::new(&state.platform);
831 let trace_path = if query_text.is_empty() {
833 format!("{method} {path}")
834 } else {
835 format!("{method} {path}?{query_text}")
836 };
837 let edge_span = trace_id.as_ref().map(|_| trace::new_span_id());
842 if let (Some(id), Some(span)) = (&trace_id, &edge_span) {
843 *edge = Some(EdgeTrace::new(
844 &state.platform,
845 id,
846 &trace_path,
847 span,
848 &parent_span,
849 ));
850 }
851 if let Some(auth_route) = &info.authentication {
854 let auth_event = build_event(
855 auth_route,
856 &http_request,
857 &cid,
858 &trace_id,
859 &trace_path,
860 &edge_span,
861 )?;
862 let verdict = po.request(auth_event, info.timeout).await?;
863 if verdict.has_error() {
864 return Err(AppError::new(
865 verdict.status(),
866 verdict
867 .body_as::<String>()
868 .unwrap_or_else(|_| "Unauthorized".to_string()),
869 ));
870 }
871 if !verdict.body_as::<bool>().unwrap_or(false) {
872 return Err(AppError::new(401, "Unauthorized"));
873 }
874 for (key, value) in verdict.headers() {
878 http_request = http_request.set_session_info(key, value);
879 }
880 }
881 let is_head = method == "HEAD";
882 let envelope_stream = !is_head && is_event_api_stream(info, &http_request);
887 let result = if (info.stream_response && !is_head) || envelope_stream {
890 match stream_dispatch(
891 state,
892 info,
893 &http_request,
894 &cid,
895 &cid_header,
896 &trace_id,
897 &trace_path,
898 &edge_span,
899 accept.clone(),
900 envelope_stream,
901 edge,
902 )
903 .await?
904 {
905 StreamOutcome::Streaming(response) => return Ok(response),
906 StreamOutcome::SingleShot(envelope) => envelope,
907 }
908 } else {
909 let context_id = uuid::Uuid::new_v4().simple().to_string();
915 let (tx, rx) = oneshot::channel();
916 pending_responses()
917 .lock()
918 .expect("pending http contexts poisoned")
919 .insert(context_id.clone(), tx);
920 let event = build_event(
921 &info.service,
922 &http_request,
923 &cid,
924 &trace_id,
925 &trace_path,
926 &edge_span,
927 )?
928 .set_correlation_id(&context_id)
929 .set_reply_to(ASYNC_HTTP_RESPONSE);
930 if let Err(e) = po.send(event).await {
931 pending_responses()
932 .lock()
933 .expect("pending http contexts poisoned")
934 .remove(&context_id);
935 return Err(e);
936 }
937 match tokio::time::timeout(info.timeout, rx).await {
938 Ok(Ok(envelope)) => envelope,
939 Ok(Err(_)) => {
940 return Err(AppError::new(500, "Response channel closed unexpectedly"));
941 }
942 Err(_) => {
943 pending_responses()
944 .lock()
945 .expect("pending http contexts poisoned")
946 .remove(&context_id);
947 return Err(AppError::new(
948 408,
949 format!("Timeout for {} ms", info.timeout.as_millis()),
950 ));
951 }
952 }
953 };
954 let status = status_of(result.status());
957 let (result, standard_error) = match result.body() {
965 rmpv::Value::String(text) if result.status() >= 400 && result.headers().is_empty() => {
966 let message = text.as_str().unwrap_or_default().trim().to_string();
967 if message.starts_with('{') || message.starts_with('[') || message.starts_with('<') {
968 (result, false)
969 } else {
970 (
971 result.set_raw_body(rmpv::Value::Map(vec![
972 (
973 rmpv::Value::from("status"),
974 rmpv::Value::from(status.as_u16()),
975 ),
976 (rmpv::Value::from("message"), rmpv::Value::from(message)),
977 (rmpv::Value::from("type"), rmpv::Value::from("error")),
978 ])),
979 true,
980 )
981 }
982 }
983 _ => (result, false),
984 };
985 let mut content_type: Option<String> = None;
986 let mut set_cookies: Vec<String> = Vec::new();
987 let mut response_headers: HashMap<String, String> = HashMap::new();
988 for (name, value) in result.headers() {
989 let key = name.to_lowercase();
990 if PROTECTED_METADATA.contains(&key.as_str()) {
993 continue;
994 }
995 match key.as_str() {
996 "x-stream-id" if value.starts_with("stream.") && value.contains(".in") => {}
1000 "x-ttl" => {}
1001 "content-type" => {
1004 if !is_head {
1005 content_type = Some(value.to_lowercase());
1006 }
1007 }
1008 "set-cookie" => {
1011 set_cookies.extend(value.split('|').map(|c| c.trim().to_string()));
1012 }
1013 _ => {
1014 response_headers.insert(key, value.clone());
1015 }
1016 }
1017 }
1018 if content_type.is_none() && !is_head {
1023 content_type = accept_fallback_type(accept.as_deref(), result.body());
1024 }
1025 if standard_error && content_type.is_none() && !is_head {
1028 content_type = Some("application/json".to_string());
1029 }
1030 let payload = render_payload(result.body(), content_type.as_deref());
1031 if let Some(header_info) = &info.headers {
1034 header_info.response.apply(&mut response_headers);
1035 }
1036 response_headers.entry(cid_header.clone()).or_insert(cid);
1041 if let Some(content_type) = content_type {
1042 response_headers.insert("content-type".to_string(), content_type);
1043 }
1044 if let Some(cors) = &info.cors {
1045 for (name, value) in &cors.headers {
1046 response_headers.insert(name.to_lowercase(), value.clone());
1047 }
1048 }
1049 let mut response = Response::builder().status(status);
1050 for (name, value) in response_headers {
1051 response = response.header(name, value);
1052 }
1053 for cookie in set_cookies {
1054 if !cookie.is_empty() {
1055 response = response.header("set-cookie", cookie);
1056 }
1057 }
1058 let payload = if is_head { Bytes::new() } else { payload };
1060 response
1061 .body(full(payload))
1062 .map_err(|e| AppError::new(500, e.to_string()))
1063}
1064
1065#[allow(clippy::large_enum_variant)]
1070enum StreamOutcome {
1071 Streaming(Response<HttpBody>),
1072 SingleShot(EventEnvelope),
1073}
1074
1075fn stream_marker(event: &EventEnvelope) -> Result<Option<&'static str>, ()> {
1079 for (name, value) in event.headers() {
1080 if name.eq_ignore_ascii_case(event_stream::X_EVENT_STREAM) {
1081 return match value.to_lowercase().as_str() {
1082 event_stream::DATA => Ok(Some(event_stream::DATA)),
1083 event_stream::EOF => Ok(Some(event_stream::EOF)),
1084 event_stream::EXCEPTION => Ok(Some(event_stream::EXCEPTION)),
1085 _ => Err(()),
1086 };
1087 }
1088 }
1089 Ok(None)
1090}
1091
1092fn stream_error_message(event: &EventEnvelope) -> String {
1094 match event.body() {
1095 rmpv::Value::Map(entries) => entries
1096 .iter()
1097 .find(|(key, _)| key.as_str() == Some("message"))
1098 .map(|(_, value)| stream_text(value))
1099 .unwrap_or_else(|| "Stream failed".to_string()),
1100 rmpv::Value::Nil => "Stream failed".to_string(),
1101 other => stream_text(other),
1102 }
1103}
1104
1105fn negotiate_stream_type(accept: Option<&str>) -> String {
1108 let Some(accept) = accept else {
1109 return "application/json".to_string();
1110 };
1111 if accept.contains("*/*") || accept.contains("application/json") {
1112 "application/json".to_string()
1113 } else if accept.contains("text/event-stream") {
1114 "text/event-stream".to_string()
1115 } else if accept.contains("text/html") {
1116 "text/html".to_string()
1117 } else if accept.contains("application/xml") {
1118 "application/xml".to_string()
1119 } else {
1120 "text/plain".to_string()
1121 }
1122}
1123
1124fn stream_text(body: &rmpv::Value) -> String {
1128 match body {
1129 rmpv::Value::Nil => String::new(),
1130 rmpv::Value::String(text) => text.as_str().unwrap_or_default().to_string(),
1131 rmpv::Value::Binary(bytes) => String::from_utf8_lossy(bytes).to_string(),
1132 other => {
1133 let stripped = crate::serializer::strip_nulls(other);
1134 let json = serde_json::to_value(&stripped).unwrap_or_default();
1135 serde_json::to_string(&json).unwrap_or_default()
1136 }
1137 }
1138}
1139
1140fn sse_frame(event_name: Option<&str>, text: &str) -> Bytes {
1143 let mut frame = String::new();
1144 if let Some(name) = event_name.filter(|n| !n.is_empty()) {
1145 frame.push_str("event: ");
1146 frame.push_str(name);
1147 frame.push('\n');
1148 }
1149 for line in text.split('\n') {
1150 frame.push_str("data: ");
1151 frame.push_str(line);
1152 frame.push('\n');
1153 }
1154 frame.push('\n');
1155 Bytes::from(frame)
1156}
1157
1158fn chunk_bytes(body: &rmpv::Value) -> Bytes {
1161 match body {
1162 rmpv::Value::Nil => Bytes::new(),
1163 rmpv::Value::String(text) => Bytes::from(text.as_str().unwrap_or_default().to_string()),
1164 rmpv::Value::Binary(bytes) => Bytes::from(bytes.clone()),
1165 other => {
1166 let mut line = stream_text(other);
1167 line.push('\n');
1168 Bytes::from(line)
1169 }
1170 }
1171}
1172
1173fn stream_event_name(event: &EventEnvelope) -> Option<&str> {
1175 event
1176 .headers()
1177 .iter()
1178 .find(|(name, _)| name.eq_ignore_ascii_case(event_stream::X_EVENT_NAME))
1179 .map(|(_, value)| value.as_str())
1180}
1181
1182fn is_event_api_stream(info: &RouteInfo, request: &crate::automation::AsyncHttpRequest) -> bool {
1188 info.service == super::event_api::EVENT_API_SERVICE
1189 && request.header("x-async") != Some("true")
1190 && request
1191 .header("accept")
1192 .is_some_and(|accept| accept.contains("text/event-stream"))
1193}
1194
1195fn event_api_idle(request: &crate::automation::AsyncHttpRequest) -> Duration {
1199 let ttl_ms = request
1200 .header("x-ttl")
1201 .and_then(|v| v.trim().parse::<u64>().ok())
1202 .unwrap_or(0)
1203 .max(1000);
1204 Duration::from_millis(ttl_ms)
1205}
1206
1207#[allow(clippy::too_many_arguments)]
1216async fn stream_dispatch(
1217 state: &RouterState,
1218 info: &RouteInfo,
1219 http_request: &crate::automation::AsyncHttpRequest,
1220 cid: &str,
1221 cid_header: &str,
1222 trace_id: &Option<String>,
1223 trace_path: &str,
1224 parent_span: &Option<String>,
1225 accept: Option<String>,
1226 envelope_mode: bool,
1227 edge: &mut Option<EdgeTrace>,
1228) -> Result<StreamOutcome, AppError> {
1229 let Some(lane) = checkout_lane() else {
1232 return Err(AppError::new(503, "Streaming response pool exhausted"));
1233 };
1234 let po = PostOffice::new(&state.platform);
1235 let context_id = uuid::Uuid::new_v4().simple().to_string();
1236 let (tx, mut rx) = mpsc::channel::<EventEnvelope>(STREAM_EVENT_BUFFER);
1237 pending_streams()
1238 .lock()
1239 .expect("pending streams poisoned")
1240 .insert(
1241 context_id.clone(),
1242 StreamContext {
1243 sender: tx,
1244 data_frames: 0,
1245 },
1246 );
1247 let event = build_event(
1248 &info.service,
1249 http_request,
1250 cid,
1251 trace_id,
1252 trace_path,
1253 parent_span,
1254 )?
1255 .set_correlation_id(&context_id)
1256 .set_reply_to(&lane);
1257 if let Err(e) = po.send(event).await {
1258 cleanup_stream(&context_id, &lane);
1259 return Err(e);
1260 }
1261 let base_idle = if envelope_mode {
1264 event_api_idle(http_request)
1265 } else {
1266 info.timeout
1267 };
1268 let (first, marker) = loop {
1270 match tokio::time::timeout(base_idle, rx.recv()).await {
1271 Ok(Some(envelope)) => match stream_marker(&envelope) {
1272 Ok(Some(marker)) => break (envelope, Some(marker)),
1273 Ok(None) => break (envelope, None),
1274 Err(()) => {
1275 log::warn!(
1277 "Dropping event for {context_id} - invalid {} signal",
1278 event_stream::X_EVENT_STREAM
1279 );
1280 }
1281 },
1282 Ok(None) => {
1283 cleanup_stream(&context_id, &lane);
1284 return Err(AppError::new(500, "Response channel closed unexpectedly"));
1285 }
1286 Err(_) => {
1287 cleanup_stream(&context_id, &lane);
1288 return Err(AppError::new(
1289 408,
1290 format!("Timeout for {} ms", base_idle.as_millis()),
1291 ));
1292 }
1293 }
1294 };
1295 let Some(marker) = marker else {
1296 cleanup_stream(&context_id, &lane);
1301 let reply = if envelope_mode {
1302 wire_single_shot(first)?
1303 } else {
1304 first
1305 };
1306 return Ok(StreamOutcome::SingleShot(reply));
1307 };
1308 if marker == event_stream::EXCEPTION && !envelope_mode {
1309 cleanup_stream(&context_id, &lane);
1313 let status = if first.status() >= 400 {
1314 first.status()
1315 } else {
1316 500
1317 };
1318 return Err(AppError::new(status, stream_error_message(&first)));
1319 }
1320 if first
1322 .headers()
1323 .keys()
1324 .any(|k| k.eq_ignore_ascii_case("x-stream-id"))
1325 {
1326 log::warn!("Ignoring x-stream-id on a streaming response for {context_id}");
1328 }
1329 let mut response_headers: HashMap<String, String> = HashMap::new();
1330 let mut set_cookies: Vec<String> = Vec::new();
1331 let mut content_type: Option<String> = None;
1332 let mut idle_override: Option<Duration> = None;
1333 for (name, value) in first.headers() {
1334 let key = name.to_lowercase();
1335 match key.as_str() {
1336 event_stream::X_EVENT_STREAM | event_stream::X_EVENT_NAME | "x-stream-id" => {}
1338 "x-ttl" => {
1340 if let Ok(seconds) = value.trim().parse::<u64>() {
1341 if seconds > 0 {
1342 idle_override = Some(Duration::from_secs(seconds));
1343 }
1344 }
1345 }
1346 _ if envelope_mode => {}
1349 "content-type" => content_type = Some(value.to_lowercase()),
1350 "set-cookie" => {
1351 set_cookies.extend(value.split('|').map(|c| c.trim().to_string()));
1352 }
1353 _ => {
1354 response_headers.insert(key, value.clone());
1355 }
1356 }
1357 }
1358 if let Some(header_info) = &info.headers {
1361 header_info.response.apply(&mut response_headers);
1362 }
1363 response_headers
1365 .entry(cid_header.to_string())
1366 .or_insert_with(|| cid.to_string());
1367 if let Some(cors) = &info.cors {
1368 for (name, value) in &cors.headers {
1369 response_headers.insert(name.to_lowercase(), value.clone());
1370 }
1371 }
1372 let content_type = if envelope_mode {
1374 "text/event-stream".to_string()
1375 } else {
1376 content_type.unwrap_or_else(|| negotiate_stream_type(accept.as_deref()))
1377 };
1378 let sse = content_type.starts_with("text/event-stream");
1379 if sse {
1380 response_headers
1382 .entry("cache-control".to_string())
1383 .or_insert_with(|| "no-cache".to_string());
1384 }
1385 let idle = idle_override.unwrap_or(base_idle);
1386 let mut builder = Response::builder().status(status_of(first.status()));
1387 for (name, value) in &response_headers {
1388 builder = builder.header(name, value);
1389 }
1390 for cookie in set_cookies {
1391 if !cookie.is_empty() {
1392 builder = builder.header("set-cookie", cookie);
1393 }
1394 }
1395 builder = builder.header("content-type", &content_type);
1396 let (body_tx, body_rx) = mpsc::channel::<Frame<Bytes>>(STREAM_FRAME_BUFFER);
1397 let response = builder
1398 .body(BoxBody::new(ChannelBody { rx: body_rx }))
1399 .map_err(|e| AppError::new(500, e.to_string()))?;
1400 let head_status = first.status();
1403 tokio::spawn(render_stream(
1404 rx,
1405 body_tx,
1406 sse,
1407 idle,
1408 context_id,
1409 lane,
1410 first,
1411 marker,
1412 envelope_mode,
1413 head_status,
1414 edge.take(),
1415 ));
1416 Ok(StreamOutcome::Streaming(response))
1417}
1418
1419#[allow(clippy::large_enum_variant)]
1423enum Waited {
1424 Event(EventEnvelope),
1425 Idle,
1426 Closed,
1427}
1428
1429async fn next_stream_event(
1433 rx: &mut mpsc::Receiver<EventEnvelope>,
1434 body_tx: &mpsc::Sender<Frame<Bytes>>,
1435 sse: bool,
1436 idle: Duration,
1437) -> Waited {
1438 let ping_every = keep_alive_ms();
1439 let idle_deadline = tokio::time::sleep(idle);
1440 tokio::pin!(idle_deadline);
1441 loop {
1442 if sse && ping_every > 0 {
1443 let ping = tokio::time::sleep(Duration::from_millis(ping_every));
1444 tokio::pin!(ping);
1445 tokio::select! {
1446 received = rx.recv() => {
1447 return match received {
1448 Some(event) => Waited::Event(event),
1449 None => Waited::Closed,
1450 };
1451 }
1452 _ = &mut idle_deadline => return Waited::Idle,
1453 _ = &mut ping => {
1454 let _ = body_tx.try_send(Frame::data(Bytes::from_static(b": ping\n\n")));
1455 }
1456 }
1457 } else {
1458 tokio::select! {
1459 received = rx.recv() => {
1460 return match received {
1461 Some(event) => Waited::Event(event),
1462 None => Waited::Closed,
1463 };
1464 }
1465 _ = &mut idle_deadline => return Waited::Idle,
1466 }
1467 }
1468 }
1469}
1470
1471async fn push_frame(
1476 body_tx: &mpsc::Sender<Frame<Bytes>>,
1477 idle: Duration,
1478 context_id: &str,
1479 bytes: Bytes,
1480) -> bool {
1481 if bytes.is_empty() {
1482 return true;
1483 }
1484 match tokio::time::timeout(idle, body_tx.send(Frame::data(bytes))).await {
1485 Ok(Ok(())) => true,
1486 Ok(Err(_)) => {
1487 log::debug!("Client disconnected from event stream {context_id}");
1488 false
1489 }
1490 Err(_) => {
1491 log::error!("Closing event stream for {context_id} - client too slow");
1492 false
1493 }
1494 }
1495}
1496
1497#[allow(clippy::too_many_arguments)]
1506async fn render_stream(
1507 mut rx: mpsc::Receiver<EventEnvelope>,
1508 body_tx: mpsc::Sender<Frame<Bytes>>,
1509 sse: bool,
1510 idle: Duration,
1511 context_id: String,
1512 lane: String,
1513 first: EventEnvelope,
1514 first_marker: &'static str,
1515 envelope_mode: bool,
1516 head_status: i32,
1517 edge: Option<EdgeTrace>,
1518) {
1519 let mut pending = Some((first, first_marker));
1520 let mut first_frame = true;
1521 let mut outcome: (i32, Option<String>) = (head_status, None);
1524 loop {
1525 let (event, marker) = match pending.take() {
1526 Some(next) => next,
1527 None => match next_stream_event(&mut rx, &body_tx, sse, idle).await {
1528 Waited::Event(event) => match stream_marker(&event) {
1529 Ok(Some(marker)) => (event, marker),
1530 Ok(None) | Err(()) => {
1531 log::warn!(
1532 "Dropping event for {context_id} - invalid {} signal",
1533 event_stream::X_EVENT_STREAM
1534 );
1535 continue;
1536 }
1537 },
1538 Waited::Idle => {
1539 outcome = (408, Some(format!("Timeout for {} seconds", idle.as_secs())));
1541 if envelope_mode {
1542 let frame = idle_timeout_envelope_frame(idle);
1543 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1544 } else if sse {
1545 let error = serde_json::json!({
1546 "status": 408,
1547 "message": format!("Timeout for {} seconds", idle.as_secs()),
1548 "type": "error",
1549 });
1550 let frame = sse_frame(Some("error"), &error.to_string());
1551 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1552 }
1553 break;
1554 }
1555 Waited::Closed => break,
1556 },
1557 };
1558 match marker {
1559 event_stream::DATA => {
1560 let bytes = if envelope_mode {
1561 envelope_mode_data_frame(&event, first_frame)
1562 } else if sse {
1563 if matches!(event.body(), rmpv::Value::Nil) {
1564 Bytes::new()
1565 } else {
1566 sse_frame(stream_event_name(&event), &stream_text(event.body()))
1567 }
1568 } else {
1569 chunk_bytes(event.body())
1570 };
1571 first_frame = false;
1572 if !push_frame(&body_tx, idle, &context_id, bytes).await {
1573 break;
1574 }
1575 }
1576 event_stream::EOF => {
1577 if envelope_mode {
1578 let frame = envelope_wire_frame(&event);
1579 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1580 } else if sse {
1581 let text = if matches!(event.body(), rmpv::Value::Nil) {
1582 "{}".to_string()
1583 } else {
1584 stream_text(event.body())
1585 };
1586 let frame = sse_frame(Some("done"), &text);
1587 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1588 }
1589 break;
1590 }
1591 _ => {
1592 outcome = (
1596 if event.status() >= 400 {
1597 event.status()
1598 } else {
1599 500
1600 },
1601 Some(stream_error_message(&event)),
1602 );
1603 if envelope_mode {
1604 let frame = envelope_wire_frame(&event);
1605 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1606 } else if sse {
1607 let status = if event.status() >= 400 {
1608 event.status()
1609 } else {
1610 500
1611 };
1612 let error = serde_json::json!({
1613 "status": status,
1614 "message": stream_error_message(&event),
1615 "type": "error",
1616 });
1617 let frame = sse_frame(Some("error"), &error.to_string());
1618 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1619 }
1620 break;
1621 }
1622 }
1623 }
1624 cleanup_stream(&context_id, &lane);
1625 if let Some(edge) = edge {
1626 edge.record(outcome.0, outcome.1).await;
1627 }
1628}
1629
1630fn envelope_mode_data_frame(event: &EventEnvelope, first_frame: bool) -> Bytes {
1635 if first_frame || !raw_streamable(event) {
1636 envelope_wire_frame(event)
1637 } else if matches!(event.body(), rmpv::Value::Nil) {
1638 Bytes::new()
1639 } else {
1640 sse_frame(stream_event_name(event), &stream_text(event.body()))
1641 }
1642}
1643
1644fn raw_streamable(event: &EventEnvelope) -> bool {
1650 if event.status() != 200 {
1651 return false;
1652 }
1653 for (name, value) in event.headers() {
1654 let key = name.to_lowercase();
1655 let reserved = key == event_stream::X_EVENT_STREAM
1656 || key == event_stream::X_EVENT_NAME
1657 || key == "x-ttl";
1658 if !reserved || (key == event_stream::X_EVENT_NAME && value == event_stream::ENVELOPE) {
1659 return false;
1660 }
1661 }
1662 match event.body() {
1663 rmpv::Value::Nil => true,
1664 rmpv::Value::String(text) => !text.as_str().unwrap_or_default().contains('\r'),
1665 _ => false,
1666 }
1667}
1668
1669fn wire_single_shot(result: EventEnvelope) -> Result<EventEnvelope, AppError> {
1673 let bytes = result.clear_to().clear_reply_to().to_bytes()?;
1674 Ok(EventEnvelope::new()
1675 .set_status(200)
1676 .set_header("content-type", "application/octet-stream")
1677 .set_raw_body(rmpv::Value::Binary(bytes)))
1678}
1679
1680fn envelope_wire_frame(event: &EventEnvelope) -> Bytes {
1685 use base64::Engine as _;
1686 let wire = event.clone().clear_to().clear_reply_to();
1687 match wire.to_bytes() {
1688 Ok(bytes) => sse_frame(
1689 Some(event_stream::ENVELOPE),
1690 &base64::engine::general_purpose::STANDARD.encode(bytes),
1691 ),
1692 Err(_) => Bytes::new(),
1693 }
1694}
1695
1696fn idle_timeout_envelope_frame(idle: Duration) -> Bytes {
1700 let message = format!("Timeout for {} seconds", idle.as_secs());
1701 let error = EventEnvelope::new()
1702 .set_header(event_stream::X_EVENT_STREAM, event_stream::EXCEPTION)
1703 .set_status(408)
1704 .set_body(serde_json::json!({"type": "error", "status": 408, "message": message}));
1705 match error {
1706 Ok(envelope) => envelope_wire_frame(&envelope),
1707 Err(_) => Bytes::new(),
1708 }
1709}
1710
1711fn build_event(
1712 to: &str,
1713 http_request: &crate::automation::AsyncHttpRequest,
1714 cid: &str,
1715 trace_id: &Option<String>,
1716 trace_path: &str,
1717 parent_span: &Option<String>,
1718) -> Result<EventEnvelope, AppError> {
1719 let mut event = EventEnvelope::new()
1720 .set_to(to)
1721 .set_from("http.request")
1722 .set_correlation_id(cid)
1723 .add_tag(crate::post_office::BUSINESS_CID_TAG, cid)
1728 .set_raw_body(http_request.to_value());
1732 if let Some(trace_id) = trace_id {
1733 event = event.set_trace(trace_id, trace_path);
1734 if let Some(parent) = parent_span {
1735 event = event.set_span_id(parent);
1737 }
1738 }
1739 Ok(event)
1740}
1741
1742enum ParsedBody {
1744 Value(serde_json::Value),
1746 Form(HashMap<String, String>),
1748 Bytes(Vec<u8>),
1750}
1751
1752fn base_content_type(headers: &HashMap<String, String>) -> Option<String> {
1757 headers
1758 .get("content-type")
1759 .map(|ct| ct.split(';').next().unwrap_or(ct).trim().to_string())
1760}
1761
1762fn parse_body(headers: &HashMap<String, String>, bytes: &Bytes) -> ParsedBody {
1779 let content_type = base_content_type(headers);
1780 let ct = content_type.as_deref().unwrap_or("?");
1781 if ct.starts_with("application/json") {
1782 let text = String::from_utf8_lossy(bytes).to_string();
1783 let trimmed = text.trim();
1784 let parsed = if trimmed.is_empty() {
1785 Some(serde_json::Value::Object(serde_json::Map::new()))
1786 } else if (trimmed.starts_with('{') && trimmed.ends_with('}'))
1787 || (trimmed.starts_with('[') && trimmed.ends_with(']'))
1788 {
1789 serde_json::from_str(&text).ok()
1790 } else {
1791 None
1792 };
1793 ParsedBody::Value(parsed.unwrap_or(serde_json::Value::String(text)))
1794 } else if ct == "application/x-www-form-urlencoded" {
1795 let text = String::from_utf8_lossy(bytes);
1796 let mut form = HashMap::new();
1797 for pair in text.split('&').filter(|p| !p.is_empty()) {
1798 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
1799 form.insert(url_decode(name), url_decode(value));
1800 }
1801 ParsedBody::Form(form)
1802 } else if ct.starts_with("application/xml")
1803 || ct.starts_with("text/html")
1804 || ct.starts_with("text/plain")
1805 {
1806 ParsedBody::Value(serde_json::Value::String(
1807 String::from_utf8_lossy(bytes).to_string(),
1808 ))
1809 } else if bytes.is_empty() {
1810 ParsedBody::Value(serde_json::Value::Null)
1811 } else {
1812 ParsedBody::Bytes(bytes.to_vec())
1813 }
1814}
1815
1816fn url_decode(text: &str) -> String {
1818 let bytes = text.as_bytes();
1819 let mut out = Vec::with_capacity(bytes.len());
1820 let mut i = 0;
1821 while i < bytes.len() {
1822 match bytes[i] {
1823 b'+' => {
1824 out.push(b' ');
1825 i += 1;
1826 }
1827 b'%' if i + 2 < bytes.len() => {
1828 let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok();
1829 match hex.and_then(|h| u8::from_str_radix(h, 16).ok()) {
1830 Some(byte) => {
1831 out.push(byte);
1832 i += 3;
1833 }
1834 None => {
1835 out.push(bytes[i]);
1836 i += 1;
1837 }
1838 }
1839 }
1840 other => {
1841 out.push(other);
1842 i += 1;
1843 }
1844 }
1845 }
1846 String::from_utf8_lossy(&out).to_string()
1847}
1848
1849const DEFAULT_REST_YAML: &str = include_str!("../../resources/default-rest.yaml");
1856
1857fn merge_default_endpoints(table: &mut RoutingTable) -> Result<(), AppError> {
1858 let defaults = RoutingTable::from_yaml_text(DEFAULT_REST_YAML)?;
1859 for route in defaults.routes() {
1860 if !table.has_url(&route.url) {
1861 table.add_route(route.clone());
1862 }
1863 }
1864 Ok(())
1865}
1866
1867async fn serve_static(
1885 state: &RouterState,
1886 path: &str,
1887 query_text: &str,
1888 headers: &HashMap<String, String>,
1889 peer: SocketAddr,
1890 head_only: bool,
1891) -> Option<Response<HttpBody>> {
1892 let (bytes, filename) = resolve_static_file(path)?;
1893 let static_content = state.table.static_content();
1894 let no_cache = super::routing::matched_element(&static_content.no_cache_pages, path);
1895 let mut filter_headers: Vec<(String, String)> = Vec::new();
1897 if let Some(filter) = &static_content.filter {
1898 let applies = super::routing::matched_element(&filter.path_list, path)
1899 && !super::routing::matched_element(&filter.exclusion_list, path);
1900 if applies {
1901 if state.platform.has_route(&filter.service) {
1902 match run_static_filter(state, filter, path, query_text, headers, peer).await {
1903 Ok(filtered) => {
1904 for (name, value) in filtered.headers() {
1906 filter_headers.push((name.clone(), value.clone()));
1907 }
1908 if filtered.status() != 200 {
1909 let (content_type, payload) = envelope_payload(&filtered);
1911 let mut response =
1912 Response::builder().status(status_of(filtered.status()));
1913 let mut has_content_type = false;
1914 for (name, value) in &filter_headers {
1915 has_content_type |= name.eq_ignore_ascii_case("content-type");
1916 response = response.header(name, value);
1917 }
1918 if let (Some(content_type), false) = (content_type, has_content_type) {
1919 response = response.header("content-type", content_type);
1920 }
1921 return response.body(full(payload)).ok();
1922 }
1923 }
1924 Err(e) => {
1925 log::error!(
1928 "Unable to filter static content HTTP-GET {} - {}",
1929 filter.service,
1930 e.message()
1931 );
1932 }
1933 }
1934 } else {
1935 log::warn!(
1936 "Static content filter {} ignored because it does not exist",
1937 filter.service
1938 );
1939 }
1940 }
1941 }
1942 let mime = mime_for(
1944 std::path::Path::new(&filename)
1945 .extension()
1946 .and_then(|e| e.to_str())
1947 .unwrap_or(""),
1948 );
1949 let mut response = Response::builder().status(StatusCode::OK);
1950 for (name, value) in &filter_headers {
1951 response = response.header(name, value);
1952 }
1953 response = response.header("content-type", mime);
1954 if no_cache {
1955 response = response
1956 .header("Cache-Control", "no-cache, no-store")
1957 .header("Pragma", "no-cache")
1958 .header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
1959 } else {
1960 use sha2::Digest;
1961 let etag = format!("\"{:x}\"", sha2::Sha256::digest(&bytes));
1962 let matched = headers
1964 .get("if-none-match")
1965 .is_some_and(|inm| inm.split(',').any(|tag| tag.trim() == etag));
1966 if matched {
1967 return Response::builder()
1968 .status(StatusCode::NOT_MODIFIED)
1969 .header("content-length", "0")
1970 .body(full(Bytes::new()))
1971 .ok();
1972 }
1973 response = response.header("ETag", etag);
1974 }
1975 let payload = if head_only {
1976 Bytes::new()
1977 } else {
1978 Bytes::from(bytes)
1979 };
1980 response.body(full(payload)).ok()
1981}
1982
1983fn resolve_static_file(path: &str) -> Option<(Vec<u8>, String)> {
1986 if path.contains("..") {
1987 return None; }
1989 let rel = path.trim_start_matches('/');
1990 let relative = if rel.is_empty() || path.ends_with('/') {
1991 format!("{rel}/index.html")
1992 .trim_start_matches('/')
1993 .to_string()
1994 } else {
1995 let filename = rel.rsplit('/').next().unwrap_or(rel);
1996 if filename.contains('.') {
1997 rel.to_string()
1998 } else {
1999 format!("{rel}.html") }
2001 };
2002 let file = crate::util::resources::resolve_classpath(&format!("public/{relative}"))?;
2003 let bytes = std::fs::read(&file).ok()?;
2004 let filename = relative.rsplit('/').next().unwrap_or(&relative).to_string();
2005 Some((bytes, filename))
2006}
2007
2008async fn run_static_filter(
2011 state: &RouterState,
2012 filter: &super::routing::SimpleHttpFilter,
2013 path: &str,
2014 query_text: &str,
2015 headers: &HashMap<String, String>,
2016 peer: SocketAddr,
2017) -> Result<EventEnvelope, AppError> {
2018 let mut request = crate::automation::AsyncHttpRequest::new()
2022 .set_method("GET")
2023 .set_url(path)
2024 .set_remote_ip(&peer.ip().to_string())
2025 .set_secure(false)
2026 .set_target_host(&headers.get("host").cloned().unwrap_or_default())
2027 .set_body(rmpv::Value::Nil);
2028 for (key, value) in headers {
2029 request = request.set_header(key, value);
2030 }
2031 for pair in query_text.split('&').filter(|p| !p.is_empty()) {
2032 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
2033 request = request.set_query_parameter(&url_decode(name), &url_decode(value));
2034 }
2035 let event = EventEnvelope::new()
2036 .set_to(&filter.service)
2037 .set_raw_body(request.to_value());
2038 let po = PostOffice::new(&state.platform);
2039 po.request(event, std::time::Duration::from_secs(10)).await
2041}
2042
2043fn accept_fallback_type(accept: Option<&str>, _body: &rmpv::Value) -> Option<String> {
2052 let accept = accept?;
2053 if accept.contains("text/html") {
2054 Some("text/html".to_string())
2055 } else if accept.contains("application/json")
2056 || accept.contains("*/*")
2057 || accept.contains("application/xml")
2058 {
2059 Some("application/json".to_string())
2060 } else {
2061 Some("text/plain".to_string())
2062 }
2063}
2064
2065fn render_payload(body: &rmpv::Value, content_type: Option<&str>) -> Bytes {
2071 match body {
2072 rmpv::Value::Nil => Bytes::new(),
2073 rmpv::Value::String(text) => Bytes::from(text.as_str().unwrap_or_default().to_string()),
2074 rmpv::Value::Binary(bytes) => Bytes::from(bytes.clone()),
2075 _ => {
2076 let stripped = crate::serializer::strip_nulls(body);
2078 let json = serde_json::to_value(&stripped).unwrap_or_default();
2079 let text = serde_json::to_string_pretty(&json).unwrap_or_default();
2086 if content_type.is_some_and(|t| t.starts_with("text/html"))
2087 && matches!(body, rmpv::Value::Map(_) | rmpv::Value::Array(_))
2088 {
2089 Bytes::from(format!("<html><body><pre>\n{text}\n</pre></body></html>"))
2090 } else {
2091 Bytes::from(text)
2092 }
2093 }
2094 }
2095}
2096
2097fn envelope_payload(result: &EventEnvelope) -> (Option<&'static str>, Bytes) {
2098 match result.body() {
2099 rmpv::Value::Nil => (None, Bytes::new()),
2100 rmpv::Value::String(text) => (
2101 Some("text/plain"),
2102 Bytes::from(text.as_str().unwrap_or_default().to_string()),
2103 ),
2104 rmpv::Value::Binary(bytes) => {
2105 (Some("application/octet-stream"), Bytes::from(bytes.clone()))
2106 }
2107 _ => {
2108 let body = crate::serializer::strip_nulls(result.body());
2110 let json = serde_json::to_value(&body).unwrap_or_default();
2111 (
2113 Some("application/json"),
2114 Bytes::from(serde_json::to_string_pretty(&json).unwrap_or_default()),
2115 )
2116 }
2117 }
2118}
2119
2120fn status_of(code: i32) -> StatusCode {
2121 StatusCode::from_u16(code as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
2122}
2123
2124fn mime_for(extension: &str) -> &'static str {
2127 match extension.to_ascii_lowercase().as_str() {
2128 "html" | "htm" => "text/html",
2129 "css" => "text/css",
2130 "js" | "mjs" => "text/javascript",
2131 "json" => "application/json",
2132 "png" => "image/png",
2133 "jpg" | "jpeg" => "image/jpeg",
2134 "gif" => "image/gif",
2135 "svg" => "image/svg+xml",
2136 "ico" => "image/x-icon",
2137 "txt" => "text/plain",
2138 "pdf" => "application/pdf",
2139 "woff2" => "font/woff2",
2140 "xml" => "application/xml",
2141 _ => "application/octet-stream",
2142 }
2143}
2144
2145fn error_response(status: i32, message: &str) -> Response<HttpBody> {
2147 let body = serde_json::json!({"status": status, "message": message, "type": "error"});
2148 Response::builder()
2149 .status(StatusCode::from_u16(status as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
2150 .header("content-type", "application/json")
2151 .body(full(Bytes::from(body.to_string())))
2152 .expect("static response")
2153}
2154
2155#[cfg(test)]
2156mod tests {
2157 use super::*;
2158
2159 #[test]
2160 fn url_decoding() {
2161 assert_eq!(url_decode("hello%20world"), "hello world");
2162 assert_eq!(url_decode("a+b"), "a b");
2163 assert_eq!(url_decode("plain"), "plain");
2164 assert_eq!(url_decode("bad%zz"), "bad%zz");
2165 }
2166
2167 fn headers_of(content_type: &str) -> HashMap<String, String> {
2168 HashMap::from([("content-type".to_string(), content_type.to_string())])
2169 }
2170
2171 fn value_of(parsed: ParsedBody) -> serde_json::Value {
2172 match parsed {
2173 ParsedBody::Value(value) => value,
2174 ParsedBody::Form(_) => panic!("expected a value, got form fields"),
2175 ParsedBody::Bytes(_) => panic!("expected a value, got bytes"),
2176 }
2177 }
2178
2179 #[test]
2182 fn body_parsing() {
2183 let json = headers_of("application/json; charset=utf-8");
2185 let value = value_of(parse_body(&json, &Bytes::from(r#"{"a":1}"#)));
2186 assert_eq!(value["a"], 1);
2187 let text = value_of(parse_body(&json, &Bytes::from("import graph from x")));
2189 assert_eq!(
2190 text,
2191 serde_json::Value::String("import graph from x".into())
2192 );
2193 let bad = value_of(parse_body(&json, &Bytes::from("{broken")));
2195 assert_eq!(bad, serde_json::Value::String("{broken".into()));
2196 let empty = value_of(parse_body(&json, &Bytes::new()));
2198 assert_eq!(empty, serde_json::json!({}));
2199 let plain = headers_of("text/plain");
2201 let unsniffed = value_of(parse_body(&plain, &Bytes::from(r#"{"a":1}"#)));
2202 assert_eq!(unsniffed, serde_json::Value::String(r#"{"a":1}"#.into()));
2203 let xml = value_of(parse_body(
2205 &headers_of("application/xml"),
2206 &Bytes::from("<a>1</a>"),
2207 ));
2208 assert_eq!(xml, serde_json::Value::String("<a>1</a>".into()));
2209 let form = parse_body(
2211 &headers_of("application/x-www-form-urlencoded"),
2212 &Bytes::from("a=1&b=hello+world"),
2213 );
2214 match form {
2215 ParsedBody::Form(fields) => {
2216 assert_eq!(fields["a"], "1");
2217 assert_eq!(fields["b"], "hello world");
2218 }
2219 _ => panic!("expected form fields"),
2220 }
2221 match parse_body(&HashMap::new(), &Bytes::from("hello")) {
2223 ParsedBody::Bytes(bytes) => assert_eq!(bytes, b"hello"),
2224 _ => panic!("expected bytes for a missing content type"),
2225 }
2226 assert_eq!(
2228 value_of(parse_body(&HashMap::new(), &Bytes::new())),
2229 serde_json::Value::Null
2230 );
2231 }
2232}