featherbit/plugins/native/
faas.rs1use std::collections::HashMap;
10
11use crate::context::Context;
12use crate::outbound::OutboundError;
13
14pub type ForwardParts = (Vec<(String, String)>, String, http::Method);
17
18pub fn forward_parts(function_uri: &str, ctx: &Context) -> Result<ForwardParts, String> {
25 let parsed: http::Uri = function_uri
26 .parse()
27 .map_err(|e| format!("invalid function_uri '{}': {}", function_uri, e))?;
28 let authority = parsed
29 .authority()
30 .map(|a| a.as_str().to_string())
31 .ok_or_else(|| format!("function_uri '{}' has no host", function_uri))?;
32 let scheme = parsed.scheme_str().unwrap_or("https");
33 let path = if parsed.path().is_empty() {
34 "/"
35 } else {
36 parsed.path()
37 };
38
39 let mut headers: Vec<(String, String)> = Vec::new();
40 for (name, values) in &ctx.request.headers {
41 let lname = name.to_ascii_lowercase();
42 if matches!(lname.as_str(), "host" | "connection" | "content-length") {
43 continue;
44 }
45 for value in values {
46 headers.push((lname.clone(), value.clone()));
47 }
48 }
49 headers.push(("host".to_string(), authority.clone()));
50
51 let query = query_string(&ctx.request.query_params);
53 let query = if query.is_empty() {
54 parsed.query().unwrap_or("").to_string()
55 } else {
56 query
57 };
58 let url = if query.is_empty() {
59 format!("{}://{}{}", scheme, authority, path)
60 } else {
61 format!("{}://{}{}?{}", scheme, authority, path, query)
62 };
63
64 let method: http::Method = ctx.request.method.parse().unwrap_or(http::Method::POST);
65
66 Ok((headers, url, method))
67}
68
69fn query_string(query: &HashMap<String, Vec<String>>) -> String {
72 let mut pairs: Vec<(String, String)> = Vec::new();
73 for (name, values) in query {
74 for value in values {
75 pairs.push((uri_encode(name), uri_encode(value)));
76 }
77 }
78 pairs.sort();
79 pairs
80 .iter()
81 .map(|(k, v)| format!("{}={}", k, v))
82 .collect::<Vec<_>>()
83 .join("&")
84}
85
86fn uri_encode(s: &str) -> String {
88 let mut out = String::with_capacity(s.len());
89 for b in s.bytes() {
90 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
91 out.push(b as char);
92 } else {
93 out.push_str(&format!("%{:02X}", b));
94 }
95 }
96 out
97}
98
99pub fn classify_error(plugin: &str, err: &OutboundError) -> (u16, String) {
102 match err {
103 OutboundError::Timeout(d) => (504, format!("{} callout timed out after {:?}", plugin, d)),
104 OutboundError::InvalidRequest(m) => (502, format!("{} request build error: {}", plugin, m)),
105 OutboundError::Transport(m) => (503, format!("{} callout failed: {}", plugin, m)),
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
113 use bytes::Bytes;
114
115 fn ctx() -> Context {
116 Context {
117 request: GatewayRequest {
118 method: "POST".to_string(),
119 path: "/orig".to_string(),
120 host: "gw".to_string(),
121 scheme: "http".to_string(),
122 headers: HashMap::new(),
123 query_params: HashMap::new(),
124 body: Bytes::new(),
125 remote_addr: "1.2.3.4:5".to_string(),
126 protocol: Protocol::Http1,
127 },
128 response: GatewayResponse {
129 status_code: 0,
130 headers: HashMap::new(),
131 body: Bytes::new(),
132 },
133 message: HashMap::new(),
134 errors: Vec::new(),
135 }
136 }
137
138 #[test]
139 fn test_forward_parts_url_and_host() {
140 let mut c = ctx();
141 c.request
142 .headers
143 .insert("x-test".to_string(), vec!["v".to_string()]);
144 c.request
145 .query_params
146 .insert("b".to_string(), vec!["2".to_string()]);
147 c.request
148 .query_params
149 .insert("a".to_string(), vec!["1".to_string()]);
150 let (headers, url, method) = forward_parts("https://host.example.com/api/fn", &c).unwrap();
151 assert_eq!(url, "https://host.example.com/api/fn?a=1&b=2");
152 assert_eq!(method, http::Method::POST);
153 let get = |n: &str| {
154 headers
155 .iter()
156 .find(|(k, _)| k == n)
157 .map(|(_, v)| v.as_str())
158 };
159 assert_eq!(get("host"), Some("host.example.com"));
160 assert_eq!(get("x-test"), Some("v"));
161 }
162
163 #[test]
164 fn test_forward_parts_rejects_bad_uri() {
165 assert!(forward_parts("not a uri", &ctx()).is_err());
166 assert!(forward_parts("/no-host", &ctx()).is_err());
167 }
168}