1use std::collections::HashMap;
37use std::sync::{Arc, OnceLock};
38use std::time::Duration;
39
40use http_body_util::{BodyExt, Full};
41use hyper::body::Bytes;
42use rmpv::Value;
43use tokio_rustls::rustls;
44use tokio_rustls::rustls::pki_types::ServerName;
45use tokio_rustls::TlsConnector;
46
47use crate::envelope::EventEnvelope;
48use crate::event_stream;
49use crate::function::AppError;
50use crate::platform::Platform;
51use crate::post_office::PostOffice;
52use crate::util::app_config_reader::AppConfigReader;
53use crate::util::w3c_trace;
54
55pub const ASYNC_HTTP_REQUEST: &str = "async.http.request";
56pub const STREAM_RELAY: &str = "stream";
60const USER_AGENT_NAME: &str = "async-http-client";
61const DEFAULT_TTL_SECONDS: u64 = 30;
62const HEADERS_TO_IGNORE: &[&str] = &[
64 "content-length",
65 "user-agent",
66 "x-stream-id",
67 "content-encoding",
68 "transfer-encoding",
69 "host",
70 "connection",
71 "upgrade-insecure-requests",
72 "accept-encoding",
73 "sec-fetch-mode",
74 "sec-fetch-site",
75 "sec-fetch-user",
76 "x-event-api",
79];
80
81#[derive(Clone, Debug)]
91pub struct AsyncHttpRequest {
92 method: Option<String>,
93 url: Option<String>,
94 target_host: Option<String>,
95 headers: Vec<(String, String)>,
96 body: Option<Value>,
99 query_parameters: Vec<(String, Value)>,
100 path_parameters: Vec<(String, String)>,
101 cookies: Vec<(String, String)>,
102 session: Vec<(String, String)>,
103 trust_all_cert: Option<bool>,
107 ip: Option<String>,
110 https: Option<bool>,
111 timeout: Option<u64>,
112 query_string: Option<String>,
113}
114
115impl<'de> serde::Deserialize<'de> for AsyncHttpRequest {
126 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127 where
128 D: serde::Deserializer<'de>,
129 {
130 let value = Value::deserialize(deserializer)?;
131 Ok(AsyncHttpRequest::from_value(&value))
132 }
133}
134
135impl serde::Serialize for AsyncHttpRequest {
136 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
137 where
138 S: serde::Serializer,
139 {
140 self.to_value().serialize(serializer)
141 }
142}
143
144impl Default for AsyncHttpRequest {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150impl AsyncHttpRequest {
151 #[allow(clippy::new_without_default)]
152 pub fn new() -> Self {
153 AsyncHttpRequest {
154 method: None,
155 url: None,
156 target_host: None,
157 headers: Vec::new(),
158 body: None,
159 query_parameters: Vec::new(),
160 path_parameters: Vec::new(),
161 cookies: Vec::new(),
162 session: Vec::new(),
163 trust_all_cert: None,
164 ip: None,
165 https: None,
166 timeout: None,
167 query_string: None,
168 }
169 }
170
171 pub fn from_value(value: &Value) -> Self {
174 let mut request = AsyncHttpRequest::new();
175 let Value::Map(entries) = value else {
176 return request;
177 };
178 let get = |key: &str| -> Option<&Value> {
179 entries
180 .iter()
181 .find(|(k, _)| k.as_str() == Some(key))
182 .map(|(_, v)| v)
183 };
184 request.method = get("method").and_then(|v| v.as_str()).map(str::to_string);
185 request.url = get("url").and_then(|v| v.as_str()).map(str::to_string);
186 request.target_host = get("host").and_then(|v| v.as_str()).map(str::to_string);
187 request.trust_all_cert = get("trust_all_cert").and_then(|v| v.as_bool());
188 request.body = get("body").cloned();
189 request.ip = get("ip").and_then(|v| v.as_str()).map(str::to_string);
191 request.https = get("https").and_then(|v| v.as_bool());
192 request.timeout = get("timeout").and_then(|v| v.as_u64());
193 request.query_string = get("query").and_then(|v| v.as_str()).map(str::to_string);
194 if let Some(Value::Map(headers)) = get("headers") {
195 for (k, v) in headers {
196 if let Some(key) = k.as_str() {
197 request.headers.push((key.to_string(), display_text(v)));
198 }
199 }
200 }
201 if let Some(Value::Map(cookies)) = get("cookies") {
202 for (k, v) in cookies {
203 if let Some(key) = k.as_str() {
204 request.cookies.push((key.to_string(), display_text(v)));
205 }
206 }
207 }
208 if let Some(Value::Map(session)) = get("session") {
209 for (k, v) in session {
210 if let Some(key) = k.as_str() {
211 request.session.push((key.to_string(), display_text(v)));
212 }
213 }
214 }
215 if let Some(Value::Map(parameters)) = get("parameters") {
216 for (k, v) in parameters {
217 match (k.as_str(), v) {
218 (Some("query"), Value::Map(query)) => {
219 for (qk, qv) in query {
220 if let Some(key) = qk.as_str() {
221 request.query_parameters.push((key.to_string(), qv.clone()));
222 }
223 }
224 }
225 (Some("path"), Value::Map(path)) => {
226 for (pk, pv) in path {
227 if let Some(key) = pk.as_str() {
228 request
229 .path_parameters
230 .push((key.to_string(), display_text(pv)));
231 }
232 }
233 }
234 _ => {}
235 }
236 }
237 }
238 request
239 }
240
241 pub fn to_value(&self) -> Value {
243 let mut map: Vec<(Value, Value)> = Vec::new();
244 if let Some(method) = &self.method {
245 map.push((Value::from("method"), Value::from(method.as_str())));
246 }
247 if let Some(url) = &self.url {
248 map.push((Value::from("url"), Value::from(url.as_str())));
249 }
250 if let Some(host) = &self.target_host {
251 map.push((Value::from("host"), Value::from(host.as_str())));
252 }
253 if let Some(trust_all_cert) = self.trust_all_cert {
254 map.push((Value::from("trust_all_cert"), Value::from(trust_all_cert)));
256 }
257 if !self.headers.is_empty() {
258 map.push((Value::from("headers"), string_pairs(&self.headers)));
259 }
260 if let Some(body) = &self.body {
261 map.push((Value::from("body"), body.clone()));
264 }
265 if let Some(ip) = &self.ip {
268 map.push((Value::from("ip"), Value::from(ip.as_str())));
269 }
270 if let Some(https) = self.https {
271 map.push((Value::from("https"), Value::from(https)));
272 }
273 if let Some(timeout) = self.timeout {
274 map.push((Value::from("timeout"), Value::from(timeout)));
275 }
276 if let Some(query) = &self.query_string {
277 map.push((Value::from("query"), Value::from(query.as_str())));
278 }
279 if !self.cookies.is_empty() {
280 map.push((Value::from("cookies"), string_pairs(&self.cookies)));
281 }
282 if !self.session.is_empty() {
283 map.push((Value::from("session"), string_pairs(&self.session)));
284 }
285 let query: Vec<(Value, Value)> = self
289 .query_parameters
290 .iter()
291 .map(|(k, v)| (Value::from(k.as_str()), v.clone()))
292 .collect();
293 let path: Vec<(Value, Value)> = self
294 .path_parameters
295 .iter()
296 .map(|(k, v)| (Value::from(k.as_str()), Value::from(v.as_str())))
297 .collect();
298 map.push((
299 Value::from("parameters"),
300 Value::Map(vec![
301 (Value::from("query"), Value::Map(query)),
302 (Value::from("path"), Value::Map(path)),
303 ]),
304 ));
305 Value::Map(map)
306 }
307
308 pub fn set_method(mut self, method: &str) -> Self {
309 self.method = Some(method.to_string());
310 self
311 }
312
313 pub fn set_url(mut self, url: &str) -> Self {
314 self.url = Some(url.to_string());
315 self
316 }
317
318 pub fn set_target_host(mut self, host: &str) -> Self {
319 self.target_host = Some(host.to_string());
320 self
321 }
322
323 pub fn set_trust_all_cert(mut self, trust_all_cert: bool) -> Self {
326 self.trust_all_cert = Some(trust_all_cert);
327 self
328 }
329
330 pub fn set_header(mut self, key: &str, value: &str) -> Self {
332 self.headers.retain(|(k, _)| !k.eq_ignore_ascii_case(key));
333 self.headers.push((key.to_string(), value.to_string()));
334 self
335 }
336
337 pub fn set_body(mut self, body: Value) -> Self {
338 self.body = Some(body);
339 self
340 }
341
342 pub fn set_query_parameter(mut self, key: &str, value: &str) -> Self {
345 self.query_parameters.retain(|(k, _)| k != key);
346 self.query_parameters
347 .push((key.to_string(), Value::from(value)));
348 self
349 }
350
351 pub fn set_query_parameter_values(mut self, key: &str, values: &[&str]) -> Self {
354 self.query_parameters.retain(|(k, _)| k != key);
355 self.query_parameters.push((
356 key.to_string(),
357 Value::Array(values.iter().map(|v| Value::from(*v)).collect()),
358 ));
359 self
360 }
361
362 pub fn set_path_parameter(mut self, key: &str, value: &str) -> Self {
363 self.path_parameters
364 .push((key.to_string(), value.to_string()));
365 self
366 }
367
368 pub fn set_cookie(mut self, key: &str, value: &str) -> Self {
370 self.cookies.retain(|(k, _)| k != key);
371 self.cookies.push((key.to_string(), value.to_string()));
372 self
373 }
374
375 pub fn set_session_info(mut self, key: &str, value: &str) -> Self {
378 self.session.retain(|(k, _)| k != key);
379 self.session.push((key.to_string(), value.to_string()));
380 self
381 }
382
383 pub fn set_remote_ip(mut self, ip: &str) -> Self {
386 self.ip = Some(ip.to_string());
387 self
388 }
389
390 pub fn set_secure(mut self, https: bool) -> Self {
392 self.https = Some(https);
393 self
394 }
395
396 pub fn set_query_string(mut self, query: &str) -> Self {
398 self.query_string = Some(query.to_string());
399 self
400 }
401
402 pub fn set_route_timeout_seconds(mut self, seconds: u64) -> Self {
409 self.timeout = Some(seconds);
410 self
411 }
412
413 pub fn set_timeout_seconds(self, timeout_seconds: u64) -> Self {
415 let ms = timeout_seconds.max(1) * 1000;
416 self.set_header("x-ttl", &ms.to_string())
417 }
418
419 pub fn method(&self) -> &str {
420 self.method.as_deref().unwrap_or("GET")
421 }
422
423 pub fn url(&self) -> &str {
425 self.url.as_deref().unwrap_or("/")
426 }
427
428 pub fn target_host(&self) -> Option<&str> {
429 self.target_host.as_deref()
430 }
431
432 pub fn trust_all_cert(&self) -> bool {
433 self.trust_all_cert.unwrap_or(false)
434 }
435
436 pub fn headers(&self) -> &[(String, String)] {
438 &self.headers
439 }
440
441 pub fn session(&self) -> &[(String, String)] {
445 &self.session
446 }
447
448 pub fn header(&self, key: &str) -> Option<&str> {
449 self.headers
450 .iter()
451 .find(|(k, _)| k.eq_ignore_ascii_case(key))
452 .map(|(_, v)| v.as_str())
453 }
454
455 pub fn body(&self) -> &Value {
456 static NIL: Value = Value::Nil;
457 self.body.as_ref().unwrap_or(&NIL)
458 }
459
460 pub fn timeout_seconds(&self) -> u64 {
465 self.header("x-ttl")
466 .and_then(|v| v.parse::<u64>().ok())
467 .map(|ms| ms.max(1).div_ceil(1000))
468 .or(self.timeout)
472 .unwrap_or(DEFAULT_TTL_SECONDS)
473 }
474
475 pub fn body_as<T: serde::de::DeserializeOwned>(&self) -> Result<T, AppError> {
478 rmpv::ext::from_value(self.body().clone())
479 .map_err(|e| AppError::new(500, format!("unable to deserialize body: {e}")))
480 }
481
482 pub fn remote_ip(&self) -> Option<&str> {
485 self.ip.as_deref()
486 }
487
488 pub fn is_secure(&self) -> bool {
492 self.https.unwrap_or(false)
493 }
494
495 pub fn query_string(&self) -> Option<&str> {
497 self.query_string.as_deref()
498 }
499
500 pub fn path_parameter(&self, key: &str) -> Option<&str> {
502 self.path_parameters
503 .iter()
504 .find(|(k, _)| k == key)
505 .map(|(_, v)| v.as_str())
506 }
507
508 pub fn path_parameters(&self) -> &[(String, String)] {
510 &self.path_parameters
511 }
512
513 pub fn query_parameter(&self, key: &str) -> Option<String> {
516 self.query_parameters
517 .iter()
518 .find(|(k, _)| k == key)
519 .map(|(_, v)| match v {
520 Value::Array(values) => values.first().map(display_text).unwrap_or_default(),
521 other => display_text(other),
522 })
523 }
524
525 pub fn query_parameters(&self, key: &str) -> Vec<String> {
528 self.query_parameters
529 .iter()
530 .find(|(k, _)| k == key)
531 .map(|(_, v)| match v {
532 Value::Array(values) => values.iter().map(display_text).collect(),
533 other => vec![display_text(other)],
534 })
535 .unwrap_or_default()
536 }
537
538 pub fn cookie(&self, key: &str) -> Option<&str> {
540 self.cookies
541 .iter()
542 .find(|(k, _)| k == key)
543 .map(|(_, v)| v.as_str())
544 }
545
546 pub fn cookies(&self) -> &[(String, String)] {
548 &self.cookies
549 }
550
551 pub fn session_info(&self, key: &str) -> Option<&str> {
553 self.session
554 .iter()
555 .find(|(k, _)| k == key)
556 .map(|(_, v)| v.as_str())
557 }
558
559 pub fn finalized_url(&self) -> String {
562 let uri = self.url.as_deref().unwrap_or("/");
563 let (without_hash, hash) = match uri.rfind('#') {
564 Some(mark) => (&uri[..mark], Some(&uri[mark + 1..])),
565 None => (uri, None),
566 };
567 let (mut raw_uri, mut query_string) = match without_hash.rfind('?') {
568 Some(mark) => (
569 without_hash[..mark].to_string(),
570 Some(without_hash[mark + 1..].to_string()),
571 ),
572 None => (without_hash.to_string(), None),
573 };
574 let qs = self.query_parameters_to_string();
575 if let Some(qs) = qs {
576 query_string = Some(match query_string {
577 Some(existing) => format!("{existing}&{qs}"),
578 None => qs,
579 });
580 }
581 for (key, value) in &self.path_parameters {
582 let token = format!("{{{key}}}");
583 if raw_uri.contains(&token) {
584 raw_uri = raw_uri.replace(&token, value);
585 }
586 }
587 let mut out = raw_uri;
588 if let Some(qs) = query_string {
589 out.push('?');
590 out.push_str(&qs);
591 }
592 if let Some(hash) = hash {
593 out.push('#');
594 out.push_str(hash);
595 }
596 out.replace(' ', "%20")
598 }
599
600 fn query_parameters_to_string(&self) -> Option<String> {
603 if self.query_parameters.is_empty() {
604 return None;
605 }
606 let mut parts: Vec<String> = Vec::new();
607 for (key, value) in &self.query_parameters {
608 match value {
609 Value::String(_) => parts.push(format!("{key}={}", display_text(value))),
610 Value::Array(items) => {
611 for item in items {
612 if matches!(item, Value::String(_)) {
613 parts.push(format!("{key}={}", display_text(item)));
614 }
615 }
616 }
617 _ => {}
618 }
619 }
620 if parts.is_empty() {
621 None
622 } else {
623 Some(parts.join("&"))
624 }
625 }
626}
627
628fn string_pairs(pairs: &[(String, String)]) -> Value {
629 Value::Map(
630 pairs
631 .iter()
632 .map(|(k, v)| (Value::from(k.as_str()), Value::from(v.as_str())))
633 .collect(),
634 )
635}
636
637fn display_text(value: &Value) -> String {
638 match value {
639 Value::String(s) => s.as_str().unwrap_or_default().to_string(),
640 Value::Nil => String::new(),
641 other => other.to_string(),
642 }
643}
644
645struct StreamTarget {
653 reply_to: String,
654 cid: String,
655 envelope_mode: bool,
656}
657
658pub(crate) async fn handle(
659 platform: &Platform,
660 _headers: HashMap<String, String>,
661 event: EventEnvelope,
662) -> Result<EventEnvelope, AppError> {
663 let po = PostOffice::new(platform);
664 let Some(reply_to) = event.reply_to().map(str::to_string) else {
665 if let Err(e) = process_request(platform, &po, &_headers, &event, None).await {
667 log::error!("Unhandled exception (no reply-to) - {}", e.message());
668 }
669 return EventEnvelope::new().set_body("ignored");
670 };
671 let cid = event.correlation_id().unwrap_or_default().to_string();
672 let envelope_mode = event.headers().iter().any(|(name, value)| {
673 name.eq_ignore_ascii_case(super::event_api::X_EVENT_API) && value == STREAM_RELAY
674 });
675 let stream_target = Some(StreamTarget {
676 reply_to: reply_to.clone(),
677 cid: cid.clone(),
678 envelope_mode,
679 });
680 let response = match process_request(platform, &po, &_headers, &event, stream_target).await {
681 Ok(None) => return EventEnvelope::new().set_body("ignored"),
683 Ok(Some(response)) => response,
684 Err(e) => EventEnvelope::new()
685 .set_status(e.status())
686 .set_raw_body(Value::from(e.message())),
687 };
688 let _ = po
689 .send(response.set_to(&reply_to).set_correlation_id(&cid))
690 .await;
691 EventEnvelope::new().set_body("ignored")
692}
693
694fn accepts_event_stream(request: &AsyncHttpRequest) -> bool {
698 request.headers().iter().any(|(name, value)| {
699 name.eq_ignore_ascii_case("accept") && value.contains("text/event-stream")
700 })
701}
702
703async fn process_request(
704 platform: &Platform,
705 po: &PostOffice,
706 invocation_headers: &HashMap<String, String>,
707 event: &EventEnvelope,
708 stream_target: Option<StreamTarget>,
709) -> Result<Option<EventEnvelope>, AppError> {
710 let request = AsyncHttpRequest::from_value(event.body());
711 let (secure, host, port) = validate_url(&request)?;
712 let uri = request.finalized_url();
713 po.annotate_trace(
714 "destination",
715 format!(
716 "{}{}",
717 request.target_host().unwrap_or_default(),
718 raw_url(&uri)
719 ),
720 );
721 let method = request.method().to_string();
722 if !matches!(
723 method.as_str(),
724 "GET" | "HEAD" | "PUT" | "POST" | "PATCH" | "DELETE" | "OPTIONS"
725 ) {
726 return Err(AppError::new(405, "Method not allowed"));
727 }
728 let connect_ms = connect_timeout_ms();
730 let stream = tokio::time::timeout(
731 Duration::from_millis(connect_ms),
732 tokio::net::TcpStream::connect((host.as_str(), port)),
733 )
734 .await
735 .map_err(|_| AppError::new(408, format!("Connection timeout for {host}:{port}")))?
736 .map_err(|e| AppError::new(500, format!("Unable to connect to {host}:{port} - {e}")))?;
737 let mut sender = if secure {
739 let connector = tls_connector(request.trust_all_cert())?;
740 let server_name = ServerName::try_from(host.clone())
741 .map_err(|e| AppError::new(400, format!("Invalid TLS server name {host} - {e}")))?;
742 let tls_stream = connector
743 .connect(server_name, stream)
744 .await
745 .map_err(|e| AppError::new(500, format!("TLS handshake failed for {host} - {e}")))?;
746 let io = hyper_util::rt::TokioIo::new(tls_stream);
747 let (sender, connection) = hyper::client::conn::http1::handshake(io)
748 .await
749 .map_err(|e| AppError::new(500, format!("HTTP handshake failed - {e}")))?;
750 tokio::spawn(async move {
751 let _ = connection.await;
752 });
753 sender
754 } else {
755 let io = hyper_util::rt::TokioIo::new(stream);
756 let (sender, connection) = hyper::client::conn::http1::handshake(io)
757 .await
758 .map_err(|e| AppError::new(500, format!("HTTP handshake failed - {e}")))?;
759 tokio::spawn(async move {
760 let _ = connection.await;
761 });
762 sender
763 };
764 let mut builder = hyper::Request::builder()
765 .method(method.as_str())
766 .uri(if uri.is_empty() { "/" } else { &uri });
767 builder = apply_headers(
768 builder,
769 invocation_headers,
770 &request,
771 event,
772 &host,
773 port,
774 secure,
775 );
776 let body_bytes = request_body_bytes(&request, &method)?;
777 let http_request = builder
778 .body(Full::new(Bytes::from(body_bytes)))
779 .map_err(|e| AppError::new(400, format!("Invalid HTTP request - {e}")))?;
780 let ttl = Duration::from_secs(request.timeout_seconds() + 1);
786 let http_response = tokio::time::timeout(ttl, sender.send_request(http_request))
787 .await
788 .map_err(|_| AppError::new(408, format!("Timeout for {} ms", ttl.as_millis())))?
789 .map_err(|e| AppError::new(500, format!("HTTP request failed - {e}")))?;
790 let mut response = EventEnvelope::new().set_status(http_response.status().as_u16() as i32);
791 let mut content_type: Option<String> = None;
792 let mut has_content_length = false;
793 for (name, value) in http_response.headers() {
794 let key = name.as_str();
795 let text = value.to_str().unwrap_or_default();
796 if key.eq_ignore_ascii_case("content-type") {
797 content_type = Some(text.to_lowercase());
798 }
799 if key.eq_ignore_ascii_case("content-length") {
800 has_content_length = true;
801 }
802 response = response.set_header(key, text);
803 }
804 let envelope_mode = stream_target.as_ref().is_some_and(|t| t.envelope_mode);
810 if let Some(target) = stream_target {
811 let sse = content_type
812 .as_deref()
813 .is_some_and(|ct| ct.starts_with("text/event-stream"));
814 if sse && accepts_event_stream(&request) {
815 let status = http_response.status().as_u16() as i32;
816 let relay_platform = platform.clone();
817 if target.envelope_mode {
818 let idle = Duration::from_secs(request.timeout_seconds().max(1) + 1);
821 tokio::spawn(relay_envelope_sse(
822 relay_platform,
823 http_response.into_body(),
824 target.reply_to,
825 target.cid,
826 idle,
827 ));
828 } else {
829 let idle = Duration::from_secs(request.timeout_seconds().max(1));
830 tokio::spawn(relay_sse(
831 relay_platform,
832 http_response.into_body(),
833 status,
834 target.reply_to,
835 target.cid,
836 idle,
837 ));
838 }
839 return Ok(None);
840 }
841 }
842 let bytes = http_response
843 .into_body()
844 .collect()
845 .await
846 .map_err(|e| AppError::new(500, format!("Unable to read HTTP response - {e}")))?
847 .to_bytes();
848 if envelope_mode {
849 return Ok(Some(decode_relay_reply(&bytes, response.status())));
852 }
853 if !has_content_length {
854 response = response.set_header("x-content-length", &bytes.len().to_string());
855 }
856 Ok(Some(response.set_raw_body(decode_response_body(
857 &bytes,
858 content_type.as_deref(),
859 ))))
860}
861
862fn decode_relay_reply(bytes: &[u8], http_status: i32) -> EventEnvelope {
867 if bytes.is_empty() {
868 return EventEnvelope::new().set_status(http_status);
869 }
870 match EventEnvelope::from_bytes(bytes) {
871 Ok(envelope) => envelope.clear_reply_to(),
872 Err(e) => rest_error_reply(bytes, http_status).unwrap_or_else(|| {
873 EventEnvelope::new()
874 .set_status(400)
875 .set_raw_body(Value::from(format!(
876 "Did you configure rest.yaml correctly? Invalid result set - {}",
877 e.message()
878 )))
879 }),
880 }
881}
882
883fn rest_error_reply(bytes: &[u8], http_status: i32) -> Option<EventEnvelope> {
886 if http_status < 400 {
887 return None;
888 }
889 let data = serde_json::from_slice::<serde_json::Value>(bytes).ok()?;
890 if data.get("type").and_then(|v| v.as_str()) != Some("error") {
891 return None;
892 }
893 let message = data.get("message").and_then(|v| v.as_str())?;
894 Some(
895 EventEnvelope::new()
896 .set_status(http_status)
897 .set_raw_body(Value::from(message)),
898 )
899}
900
901#[derive(Default)]
906struct SseParser {
907 pending: Vec<u8>,
908 data_lines: Vec<String>,
909 event_name: Option<String>,
910}
911
912impl SseParser {
913 fn feed(&mut self, chunk: &[u8]) -> Vec<(Option<String>, String)> {
915 self.pending.extend_from_slice(chunk);
916 let mut events = Vec::new();
917 let mut start = 0;
918 let buffer = std::mem::take(&mut self.pending);
919 for i in 0..buffer.len() {
920 if buffer[i] == b'\n' {
921 let end = if i > start && buffer[i - 1] == b'\r' {
922 i - 1
923 } else {
924 i
925 };
926 let line = String::from_utf8_lossy(&buffer[start..end]).to_string();
927 start = i + 1;
928 if line.is_empty() {
929 if !self.data_lines.is_empty() {
931 events.push((self.event_name.take(), self.data_lines.join("\n")));
932 }
933 self.data_lines.clear();
934 self.event_name = None;
935 } else if !line.starts_with(':') {
936 let (field, value) = match line.find(':') {
938 Some(colon) => (&line[..colon], &line[colon + 1..]),
939 None => (line.as_str(), ""),
940 };
941 let value = value.strip_prefix(' ').unwrap_or(value);
942 match field {
943 "data" => self.data_lines.push(value.to_string()),
944 "event" => self.event_name = Some(value.to_string()),
945 _ => { }
946 }
947 }
948 }
949 }
950 self.pending = buffer[start..].to_vec();
951 events
952 }
953}
954
955async fn relay_sse(
961 platform: Platform,
962 mut body: hyper::body::Incoming,
963 status: i32,
964 reply_to: String,
965 cid: String,
966 idle: Duration,
967) {
968 let po = PostOffice::new(&platform);
969 let mut parser = SseParser::default();
970 let mut head_sent = false;
971 loop {
972 match tokio::time::timeout(idle, body.frame()).await {
973 Ok(Some(Ok(frame))) => {
974 if let Some(data) = frame.data_ref() {
975 for (name, text) in parser.feed(data) {
976 let mut segment = EventEnvelope::new()
977 .set_header(event_stream::X_EVENT_STREAM, event_stream::DATA);
978 if let Some(name) = name.filter(|n| !n.is_empty()) {
979 segment = segment.set_header(event_stream::X_EVENT_NAME, &name);
980 }
981 segment = match segment.set_body(text) {
982 Ok(seg) => seg,
983 Err(_) => continue,
984 };
985 if !head_sent {
986 head_sent = true;
987 segment = segment
990 .set_status(status)
991 .set_header("content-type", "text/event-stream");
992 }
993 if send_segment(&po, segment, &reply_to, &cid).await.is_err() {
994 return;
995 }
996 }
997 }
998 }
999 Ok(None) => {
1000 let mut eof = EventEnvelope::new()
1003 .set_header(event_stream::X_EVENT_STREAM, event_stream::EOF);
1004 if !head_sent {
1005 eof = eof
1006 .set_status(status)
1007 .set_header("content-type", "text/event-stream");
1008 }
1009 let _ = send_segment(&po, eof, &reply_to, &cid).await;
1010 return;
1011 }
1012 Ok(Some(Err(e))) => {
1013 fail_in_band(&po, &reply_to, &cid, 500, &e.to_string(), head_sent).await;
1014 return;
1015 }
1016 Err(_) => {
1017 let message = format!("Timeout for {} seconds", idle.as_secs());
1019 fail_in_band(&po, &reply_to, &cid, 408, &message, head_sent).await;
1020 return;
1021 }
1022 }
1023 }
1024}
1025
1026async fn relay_envelope_sse(
1035 platform: Platform,
1036 mut body: hyper::body::Incoming,
1037 reply_to: String,
1038 cid: String,
1039 idle: Duration,
1040) {
1041 let po = PostOffice::new(&platform);
1042 let mut parser = SseParser::default();
1043 let mut head_seen = false;
1044 loop {
1045 match tokio::time::timeout(idle, body.frame()).await {
1046 Ok(Some(Ok(frame))) => {
1047 if let Some(data) = frame.data_ref() {
1048 for (name, text) in parser.feed(data) {
1049 match relay_envelope_event(&po, name, text, &reply_to, &cid, &mut head_seen)
1050 .await
1051 {
1052 RelayFlow::Next => {}
1053 RelayFlow::End => return,
1057 }
1058 }
1059 }
1060 }
1061 Ok(None) => {
1062 fail_in_band(
1065 &po,
1066 &reply_to,
1067 &cid,
1068 500,
1069 "Event stream ended without eof",
1070 head_seen,
1071 )
1072 .await;
1073 return;
1074 }
1075 Ok(Some(Err(e))) => {
1076 fail_in_band(&po, &reply_to, &cid, 500, &e.to_string(), head_seen).await;
1077 return;
1078 }
1079 Err(_) => {
1080 let message = format!("Timeout for {} seconds", idle.as_secs());
1081 fail_in_band(&po, &reply_to, &cid, 408, &message, head_seen).await;
1082 return;
1083 }
1084 }
1085 }
1086}
1087
1088enum RelayFlow {
1090 Next,
1091 End,
1092}
1093
1094async fn relay_envelope_event(
1095 po: &PostOffice,
1096 name: Option<String>,
1097 text: String,
1098 reply_to: &str,
1099 cid: &str,
1100 head_seen: &mut bool,
1101) -> RelayFlow {
1102 use base64::Engine as _;
1103 if name.as_deref() == Some(event_stream::ENVELOPE) {
1104 let decoded = base64::engine::general_purpose::STANDARD
1105 .decode(&text)
1106 .ok()
1107 .and_then(|bytes| EventEnvelope::from_bytes(&bytes).ok());
1108 let Some(decoded) = decoded else {
1109 fail_in_band(
1110 po,
1111 reply_to,
1112 cid,
1113 500,
1114 "Invalid event stream - malformed envelope frame",
1115 *head_seen,
1116 )
1117 .await;
1118 return RelayFlow::End;
1119 };
1120 *head_seen = true;
1121 let terminal = decoded.headers().iter().any(|(key, value)| {
1122 key.eq_ignore_ascii_case(event_stream::X_EVENT_STREAM)
1123 && (value.eq_ignore_ascii_case(event_stream::EOF)
1124 || value.eq_ignore_ascii_case(event_stream::EXCEPTION))
1125 });
1126 let _ = send_segment(po, decoded.clear_reply_to(), reply_to, cid).await;
1127 if terminal {
1128 RelayFlow::End
1129 } else {
1130 RelayFlow::Next
1131 }
1132 } else if !*head_seen {
1133 fail_in_band(
1135 po,
1136 reply_to,
1137 cid,
1138 500,
1139 "Invalid event stream - missing envelope head",
1140 false,
1141 )
1142 .await;
1143 RelayFlow::End
1144 } else {
1145 let mut segment =
1147 EventEnvelope::new().set_header(event_stream::X_EVENT_STREAM, event_stream::DATA);
1148 if let Some(name) = name.filter(|n| !n.is_empty()) {
1149 segment = segment.set_header(event_stream::X_EVENT_NAME, &name);
1150 }
1151 if let Ok(segment) = segment.set_body(text) {
1152 let _ = send_segment(po, segment, reply_to, cid).await;
1153 }
1154 RelayFlow::Next
1155 }
1156}
1157
1158async fn fail_in_band(
1159 po: &PostOffice,
1160 reply_to: &str,
1161 cid: &str,
1162 status: i32,
1163 message: &str,
1164 head_sent: bool,
1165) {
1166 let body = serde_json::json!({"type": "error", "status": status, "message": message});
1168 let Ok(mut error) = EventEnvelope::new()
1169 .set_header(event_stream::X_EVENT_STREAM, event_stream::EXCEPTION)
1170 .set_status(status)
1171 .set_body(body)
1172 else {
1173 return;
1174 };
1175 if !head_sent {
1176 error = error.set_header("content-type", "text/event-stream");
1177 }
1178 let _ = send_segment(po, error, reply_to, cid).await;
1179}
1180
1181async fn send_segment(
1182 po: &PostOffice,
1183 segment: EventEnvelope,
1184 reply_to: &str,
1185 cid: &str,
1186) -> Result<(), AppError> {
1187 po.send(segment.set_to(reply_to).set_correlation_id(cid))
1188 .await
1189}
1190
1191fn raw_url(uri: &str) -> &str {
1192 match uri.rfind('?') {
1193 Some(mark) => &uri[..mark],
1194 None => uri,
1195 }
1196}
1197
1198fn connect_timeout_ms() -> u64 {
1199 let config = AppConfigReader::get_instance();
1200 config
1201 .get_property_or("http.client.connection.timeout", "5000")
1202 .parse::<u64>()
1203 .unwrap_or(5000)
1204 .max(2000)
1205}
1206
1207fn validate_url(request: &AsyncHttpRequest) -> Result<(bool, String, u16), AppError> {
1210 let Some(target) = request.target_host() else {
1211 return Err(AppError::new(
1212 400,
1213 "Missing target host. e.g. https://hostname",
1214 ));
1215 };
1216 let (secure, rest) = if let Some(rest) = target.strip_prefix("http://") {
1217 (false, rest)
1218 } else if let Some(rest) = target.strip_prefix("https://") {
1219 (true, rest)
1220 } else {
1221 return Err(AppError::new(400, "Protocol must be http or https"));
1222 };
1223 let authority = rest.trim_end_matches('/');
1224 if authority.contains('/') {
1225 return Err(AppError::new(400, "Target host must not contain URI path"));
1226 }
1227 let default_port = if secure { 443 } else { 80 };
1228 let (host, port) = match authority.rsplit_once(':') {
1229 Some((h, p)) => (
1230 h.to_string(),
1231 p.parse::<u16>()
1232 .map_err(|_| AppError::new(400, "Invalid port number in target host"))?,
1233 ),
1234 None => (authority.to_string(), default_port),
1235 };
1236 if host.trim().is_empty() {
1237 return Err(AppError::new(
1238 400,
1239 "Unable to resolve target host as domain or IP address",
1240 ));
1241 }
1242 Ok((secure, host, port))
1243}
1244
1245fn tls_connector(trust_all_cert: bool) -> Result<TlsConnector, AppError> {
1251 static STRICT: OnceLock<Result<Arc<rustls::ClientConfig>, String>> = OnceLock::new();
1252 static TRUST_ALL: OnceLock<Arc<rustls::ClientConfig>> = OnceLock::new();
1253 let config = if trust_all_cert {
1254 TRUST_ALL
1255 .get_or_init(|| {
1256 let config = rustls::ClientConfig::builder()
1257 .dangerous()
1258 .with_custom_certificate_verifier(Arc::new(TrustAllVerifier))
1259 .with_no_client_auth();
1260 Arc::new(config)
1261 })
1262 .clone()
1263 } else {
1264 STRICT
1265 .get_or_init(|| {
1266 let loaded = rustls_native_certs::load_native_certs();
1267 let mut roots = rustls::RootCertStore::empty();
1268 for cert in loaded.certs {
1269 let _ = roots.add(cert);
1272 }
1273 if roots.is_empty() {
1274 return Err(format!(
1275 "No usable certificates in the OS trust store - {:?}",
1276 loaded.errors
1277 ));
1278 }
1279 Ok(Arc::new(
1280 rustls::ClientConfig::builder()
1281 .with_root_certificates(roots)
1282 .with_no_client_auth(),
1283 ))
1284 })
1285 .clone()
1286 .map_err(|e| AppError::new(500, e))?
1287 };
1288 Ok(TlsConnector::from(config))
1289}
1290
1291#[derive(Debug)]
1295struct TrustAllVerifier;
1296
1297impl rustls::client::danger::ServerCertVerifier for TrustAllVerifier {
1298 fn verify_server_cert(
1299 &self,
1300 _end_entity: &rustls::pki_types::CertificateDer<'_>,
1301 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
1302 _server_name: &ServerName<'_>,
1303 _ocsp_response: &[u8],
1304 _now: rustls::pki_types::UnixTime,
1305 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
1306 Ok(rustls::client::danger::ServerCertVerified::assertion())
1307 }
1308
1309 fn verify_tls12_signature(
1310 &self,
1311 message: &[u8],
1312 cert: &rustls::pki_types::CertificateDer<'_>,
1313 dss: &rustls::DigitallySignedStruct,
1314 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1315 rustls::crypto::verify_tls12_signature(
1316 message,
1317 cert,
1318 dss,
1319 &rustls::crypto::ring::default_provider().signature_verification_algorithms,
1320 )
1321 }
1322
1323 fn verify_tls13_signature(
1324 &self,
1325 message: &[u8],
1326 cert: &rustls::pki_types::CertificateDer<'_>,
1327 dss: &rustls::DigitallySignedStruct,
1328 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1329 rustls::crypto::verify_tls13_signature(
1330 message,
1331 cert,
1332 dss,
1333 &rustls::crypto::ring::default_provider().signature_verification_algorithms,
1334 )
1335 }
1336
1337 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
1338 rustls::crypto::ring::default_provider()
1339 .signature_verification_algorithms
1340 .supported_schemes()
1341 }
1342}
1343
1344#[allow(clippy::too_many_arguments)]
1345fn apply_headers(
1346 mut builder: hyper::http::request::Builder,
1347 invocation_headers: &HashMap<String, String>,
1348 request: &AsyncHttpRequest,
1349 event: &EventEnvelope,
1350 host: &str,
1351 port: u16,
1352 secure: bool,
1353) -> hyper::http::request::Builder {
1354 let default_port = if secure { 443 } else { 80 };
1356 let authority = if port == default_port {
1357 host.to_string()
1358 } else {
1359 format!("{host}:{port}")
1360 };
1361 builder = builder.header("host", authority);
1362 builder = builder.header("user-agent", USER_AGENT_NAME);
1363 let mut merged: Vec<(String, String)> = request.headers.clone();
1365 merged.extend(request.session.iter().cloned());
1366 for (key, value) in &merged {
1367 if permitted_http_header(key) {
1368 builder = builder.header(key.as_str(), value.trim());
1372 }
1373 }
1374 if !merged.iter().any(|(k, _)| k.eq_ignore_ascii_case("accept")) {
1379 builder = builder.header("accept", "*/*");
1380 }
1381 let config = AppConfigReader::get_instance();
1390 if let Some(trace_id) = event.trace_id() {
1391 let trace_header = config.get_property_or("http.trace.id.header", "X-Trace-Id");
1392 stamp_header(&mut builder, &trace_header, trace_id);
1393 if let Some(traceparent) = w3c_trace::format(trace_id, event.span_id().unwrap_or_default())
1394 {
1395 stamp_header(&mut builder, w3c_trace::TRACEPARENT, &traceparent);
1396 let custom_traceparent =
1401 config.get_property_or("http.traceparent.header", w3c_trace::TRACEPARENT);
1402 if !custom_traceparent.eq_ignore_ascii_case(w3c_trace::TRACEPARENT) {
1403 stamp_header(&mut builder, &custom_traceparent, &traceparent);
1404 }
1405 }
1406 }
1407 if request.header(super::event_api::X_EVENT_API).is_none() {
1413 if let Some(business_cid) = invocation_headers.get(crate::automation::MY_CORRELATION_ID) {
1414 let cid_header =
1415 config.get_property_or("http.correlation.id.header", "X-Correlation-Id");
1416 if request.header(&cid_header).is_none() {
1417 stamp_header(&mut builder, &cid_header, business_cid.as_str());
1418 }
1419 }
1420 }
1421 if !request.cookies.is_empty() {
1423 let cookie = request
1424 .cookies
1425 .iter()
1426 .map(|(k, v)| format!("{k}={}", url_encode(v)))
1427 .collect::<Vec<_>>()
1428 .join("; ");
1429 builder = builder.header("cookie", cookie.as_str());
1430 }
1431 builder
1432}
1433
1434fn permitted_http_header(header: &str) -> bool {
1435 !HEADERS_TO_IGNORE
1436 .iter()
1437 .any(|ignored| header.eq_ignore_ascii_case(ignored))
1438}
1439
1440fn stamp_header(builder: &mut hyper::http::request::Builder, name: &str, value: &str) {
1446 if let Some(headers) = builder.headers_mut() {
1447 if let (Ok(name), Ok(value)) = (
1448 hyper::header::HeaderName::from_bytes(name.as_bytes()),
1449 hyper::header::HeaderValue::from_str(value),
1450 ) {
1451 headers.insert(name, value);
1452 }
1453 }
1454}
1455
1456fn url_encode(text: &str) -> String {
1457 let mut out = String::with_capacity(text.len());
1458 for byte in text.bytes() {
1459 match byte {
1460 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'*' => {
1461 out.push(byte as char)
1462 }
1463 b' ' => out.push('+'),
1464 other => out.push_str(&format!("%{other:02X}")),
1465 }
1466 }
1467 out
1468}
1469
1470fn request_body_bytes(request: &AsyncHttpRequest, method: &str) -> Result<Vec<u8>, AppError> {
1471 if !matches!(method, "POST" | "PUT" | "PATCH") {
1472 return Ok(Vec::new());
1473 }
1474 match request.body() {
1475 Value::Nil => Ok(Vec::new()),
1476 Value::Binary(bytes) => Ok(bytes.clone()),
1477 Value::String(text) => Ok(text.as_str().unwrap_or_default().as_bytes().to_vec()),
1478 value @ (Value::Map(_) | Value::Array(_)) => {
1479 let json = serde_json::to_value(crate::serializer::strip_nulls(value))
1483 .map_err(|e| AppError::new(400, format!("Invalid HTTP request body - {e}")))?;
1484 serde_json::to_vec(&json)
1485 .map_err(|e| AppError::new(400, format!("Invalid HTTP request body - {e}")))
1486 }
1487 _ => Err(AppError::new(400, "Invalid HTTP request body")),
1488 }
1489}
1490
1491fn decode_response_body(bytes: &[u8], content_type: Option<&str>) -> Value {
1495 let Some(content_type) = content_type else {
1496 return Value::from(bytes.to_vec());
1497 };
1498 if content_type.starts_with("application/json") {
1499 let text = String::from_utf8_lossy(bytes).trim().to_string();
1500 if text.is_empty() {
1501 return Value::Map(vec![]);
1502 }
1503 if (text.starts_with('{') && text.ends_with('}'))
1504 || (text.starts_with('[') && text.ends_with(']'))
1505 {
1506 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
1507 if let Ok(value) = rmpv::ext::to_value(&json) {
1508 return value;
1509 }
1510 }
1511 }
1512 return Value::from(text);
1513 }
1514 if content_type.starts_with("text/")
1515 || content_type.starts_with("application/javascript")
1516 || content_type.starts_with("application/xml")
1517 {
1518 return Value::from(String::from_utf8_lossy(bytes).to_string());
1519 }
1520 Value::from(bytes.to_vec())
1521}
1522
1523pub struct AsyncHttpClientService {
1528 platform: Platform,
1529}
1530
1531impl AsyncHttpClientService {
1532 pub fn new(platform: &Platform) -> Self {
1533 AsyncHttpClientService {
1534 platform: platform.clone(),
1535 }
1536 }
1537}
1538
1539#[async_trait::async_trait]
1540impl crate::function::ComposableFunction for AsyncHttpClientService {
1541 async fn handle_event(
1542 &self,
1543 headers: HashMap<String, String>,
1544 input: EventEnvelope,
1545 _instance: usize,
1546 ) -> Result<EventEnvelope, AppError> {
1547 handle(&self.platform, headers, input).await
1551 }
1552}
1553
1554#[cfg(test)]
1555mod tests {
1556 use super::*;
1557
1558 fn server_dataset_request() -> AsyncHttpRequest {
1563 AsyncHttpRequest::new()
1564 .set_method("POST")
1565 .set_url("/api/typed/alice")
1566 .set_remote_ip("127.0.0.1")
1567 .set_secure(true)
1568 .set_target_host("localhost:8085")
1569 .set_header("accept", "application/json")
1570 .set_header("x-api-key", "open-sesame")
1571 .set_header("x-ttl", "5000")
1572 .set_path_parameter("user", "alice")
1573 .set_query_parameter_values("q", &["a", "b"])
1574 .set_query_parameter("single", "1")
1575 .set_query_string("q=a&q=b&single=1")
1576 .set_route_timeout_seconds(30)
1577 .set_cookie("first", "alpha")
1578 .set_session_info("user_id", "u-1")
1579 .set_body(Value::Map(vec![(
1580 Value::from("note"),
1581 Value::from("hello"),
1582 )]))
1583 }
1584
1585 #[test]
1597 fn server_dataset_round_trips_through_the_typed_contract() {
1598 let request = server_dataset_request();
1599 assert_eq!(request.method(), "POST");
1601 assert_eq!(request.url(), "/api/typed/alice");
1602 assert_eq!(request.remote_ip(), Some("127.0.0.1"));
1603 assert!(request.is_secure());
1604 assert_eq!(request.target_host(), Some("localhost:8085"));
1605 assert_eq!(request.path_parameter("user"), Some("alice"));
1606 assert_eq!(request.query_parameter("single").as_deref(), Some("1"));
1607 assert_eq!(request.query_parameter("q").as_deref(), Some("a"));
1608 assert_eq!(request.query_parameters("q"), vec!["a", "b"]);
1609 assert_eq!(request.query_parameters("single"), vec!["1"]);
1610 assert_eq!(request.query_string(), Some("q=a&q=b&single=1"));
1611 assert_eq!(
1612 request.header("Accept"),
1613 Some("application/json"),
1614 "case-insensitive"
1615 );
1616 assert_eq!(request.cookie("first"), Some("alpha"));
1617 assert_eq!(request.session_info("user_id"), Some("u-1"));
1618 assert_eq!(request.timeout_seconds(), 5);
1620 #[derive(serde::Deserialize)]
1621 struct Note {
1622 note: String,
1623 }
1624 assert_eq!(request.body_as::<Note>().expect("typed body").note, "hello");
1625 let round = AsyncHttpRequest::from_value(&request.to_value());
1627 assert_eq!(round.to_value(), request.to_value());
1628 let deserialized: AsyncHttpRequest =
1630 rmpv::ext::from_value(request.to_value()).expect("serde deserialize");
1631 assert_eq!(deserialized.to_value(), request.to_value());
1632 let serialized = rmpv::ext::to_value(&request).expect("serde serialize");
1633 assert_eq!(
1634 AsyncHttpRequest::from_value(&serialized).to_value(),
1635 request.to_value()
1636 );
1637 }
1638
1639 #[test]
1643 fn route_timeout_is_the_fallback_without_x_ttl() {
1644 let request = AsyncHttpRequest::new()
1645 .set_method("GET")
1646 .set_url("/x")
1647 .set_route_timeout_seconds(30);
1648 assert_eq!(request.timeout_seconds(), 30);
1649 assert_eq!(
1651 AsyncHttpRequest::from_value(&request.to_value()).timeout_seconds(),
1652 30
1653 );
1654 }
1655}