1use std::{
2 borrow::Cow,
3 net::SocketAddr,
4 str::{self, FromStr},
5};
6
7use anyhow::Result;
8use http::Uri;
9use hyper::Request;
10use spin_factor_outbound_networking::config::allowed_hosts::is_service_chaining_host;
11use spin_http::routes::RouteMatch;
12
13use crate::Body;
14
15pub const FULL_URL: [&str; 2] = ["SPIN_FULL_URL", "X_FULL_URL"];
21pub const PATH_INFO: [&str; 2] = ["SPIN_PATH_INFO", "PATH_INFO"];
22pub const MATCHED_ROUTE: [&str; 2] = ["SPIN_MATCHED_ROUTE", "X_MATCHED_ROUTE"];
23pub const COMPONENT_ROUTE: [&str; 2] = ["SPIN_COMPONENT_ROUTE", "X_COMPONENT_ROUTE"];
24pub const RAW_COMPONENT_ROUTE: [&str; 2] = ["SPIN_RAW_COMPONENT_ROUTE", "X_RAW_COMPONENT_ROUTE"];
25pub const BASE_PATH: [&str; 2] = ["SPIN_BASE_PATH", "X_BASE_PATH"];
26pub const CLIENT_ADDR: [&str; 2] = ["SPIN_CLIENT_ADDR", "X_CLIENT_ADDR"];
27
28pub type HeaderPair<'a> = ([Cow<'static, str>; 2], Cow<'a, str>);
30
31pub fn compute_default_headers<'a>(
33 uri: &Uri,
34 host: &str,
35 route_match: &'a RouteMatch,
36 client_addr: SocketAddr,
37) -> anyhow::Result<Vec<HeaderPair<'a>>> {
38 fn owned(strs: &[&'static str; 2]) -> [Cow<'static, str>; 2] {
39 [strs[0].into(), strs[1].into()]
40 }
41
42 let owned_full_url = owned(&FULL_URL);
43 let owned_path_info = owned(&PATH_INFO);
44 let owned_matched_route = owned(&MATCHED_ROUTE);
45 let owned_component_route = owned(&COMPONENT_ROUTE);
46 let owned_raw_component_route = owned(&RAW_COMPONENT_ROUTE);
47 let owned_base_path = owned(&BASE_PATH);
48 let owned_client_addr = owned(&CLIENT_ADDR);
49
50 let mut res = vec![];
51 let abs_path = uri
52 .path_and_query()
53 .expect("cannot get path and query")
54 .as_str();
55
56 let path_info = route_match.trailing_wildcard();
57
58 let scheme = uri.scheme_str().unwrap_or("http");
59
60 let full_url = format!("{scheme}://{host}{abs_path}");
61
62 res.push((owned_path_info, path_info));
63 res.push((owned_full_url, full_url.into()));
64 res.push((owned_matched_route, route_match.based_route().into()));
65
66 res.push((owned_base_path, "/".into()));
67 res.push((owned_raw_component_route, route_match.raw_route().into()));
68 res.push((
69 owned_component_route,
70 route_match.raw_route_or_prefix().into(),
71 ));
72 res.push((owned_client_addr, client_addr.to_string().into()));
73
74 for (wild_name, wild_value) in route_match.named_wildcards() {
75 let wild_header = format!("SPIN_PATH_MATCH_{}", wild_name.to_ascii_uppercase()).into();
76 let wild_wagi_header = format!("X_PATH_MATCH_{}", wild_name.to_ascii_uppercase()).into();
77 res.push(([wild_header, wild_wagi_header], wild_value.into()));
78 }
79
80 Ok(res)
81}
82
83pub fn strip_forbidden_headers(req: &mut Request<Body>) {
84 let headers = req.headers_mut();
85 if let Some(host_header) = headers.get("Host")
86 && let Ok(host) = host_header.to_str()
87 && is_service_chaining_host(host)
88 {
89 headers.remove("Host");
90 }
91 headers.remove("Connection");
94}
95
96pub fn prepare_request_headers(
97 req: &Request<Body>,
98 route_match: &RouteMatch,
99 client_addr: SocketAddr,
100) -> Result<Vec<(String, String)>> {
101 let mut res = Vec::new();
102 for (name, value) in req
103 .headers()
104 .iter()
105 .map(|(name, value)| (name.to_string(), std::str::from_utf8(value.as_bytes())))
106 {
107 let value = value?.to_string();
108 res.push((name, value));
109 }
110
111 let default_host = http::HeaderValue::from_str("localhost")?;
112 let host = std::str::from_utf8(
113 req.headers()
114 .get("host")
115 .unwrap_or(&default_host)
116 .as_bytes(),
117 )?;
118
119 for (keys, val) in compute_default_headers(req.uri(), host, route_match, client_addr)? {
123 res.push((prepare_header_key(&keys[0]), val.into_owned()));
124 }
125
126 Ok(res)
127}
128
129pub fn append_headers(
130 map: &mut http::HeaderMap,
131 headers: Option<Vec<(String, String)>>,
132) -> Result<()> {
133 if let Some(src) = headers {
134 for (k, v) in src.iter() {
135 map.insert(
136 http::header::HeaderName::from_str(k)?,
137 http::header::HeaderValue::from_str(v)?,
138 );
139 }
140 };
141
142 Ok(())
143}
144
145fn prepare_header_key(key: &str) -> String {
146 key.replace('_', "-").to_ascii_lowercase()
147}
148
149#[cfg(test)]
150mod tests {
151 use std::borrow::Cow;
152
153 use super::*;
154 use anyhow::Result;
155 use spin_http::routes::Router;
156
157 #[test]
158 fn test_spin_header_keys() {
159 assert_eq!(
160 prepare_header_key("SPIN_FULL_URL"),
161 "spin-full-url".to_string()
162 );
163 assert_eq!(
164 prepare_header_key("SPIN_PATH_INFO"),
165 "spin-path-info".to_string()
166 );
167 assert_eq!(
168 prepare_header_key("SPIN_RAW_COMPONENT_ROUTE"),
169 "spin-raw-component-route".to_string()
170 );
171 }
172
173 #[test]
174 fn test_default_headers() -> Result<()> {
175 let scheme = "https";
176 let host = "fermyon.dev";
177 let trigger_route = "/foo/...";
178 let component_path = "/foo";
179 let path_info = "/bar";
180 let client_addr: SocketAddr = "127.0.0.1:8777".parse().unwrap();
181
182 let req_uri = format!(
183 "{}://{}{}{}?key1=value1&key2=value2",
184 scheme, host, component_path, path_info
185 );
186
187 let req = http::Request::builder()
188 .method("POST")
189 .uri(req_uri)
190 .body("")?;
191
192 let router = Router::build(
193 "/",
194 [(
195 &spin_http::routes::TriggerLookupKey::Component("DUMMY".into()),
196 &trigger_route.into(),
197 )],
198 None,
199 )?;
200 let route_match = router.route("/foo/bar")?;
201
202 let default_headers = compute_default_headers(req.uri(), host, &route_match, client_addr)?;
203
204 assert_eq!(
205 search(&FULL_URL, &default_headers).unwrap(),
206 "https://fermyon.dev/foo/bar?key1=value1&key2=value2".to_string()
207 );
208 assert_eq!(
209 search(&PATH_INFO, &default_headers).unwrap(),
210 "/bar".to_string()
211 );
212 assert_eq!(
213 search(&MATCHED_ROUTE, &default_headers).unwrap(),
214 "/foo/...".to_string()
215 );
216 assert_eq!(
217 search(&BASE_PATH, &default_headers).unwrap(),
218 "/".to_string()
219 );
220 assert_eq!(
221 search(&RAW_COMPONENT_ROUTE, &default_headers).unwrap(),
222 "/foo/...".to_string()
223 );
224 assert_eq!(
225 search(&COMPONENT_ROUTE, &default_headers).unwrap(),
226 "/foo".to_string()
227 );
228 assert_eq!(
229 search(&CLIENT_ADDR, &default_headers).unwrap(),
230 "127.0.0.1:8777".to_string()
231 );
232
233 Ok(())
234 }
235
236 #[test]
237 fn test_default_headers_with_named_wildcards() -> Result<()> {
238 let scheme = "https";
239 let host = "fermyon.dev";
240 let trigger_route = "/foo/:userid/...";
241 let component_path = "/foo";
242 let path_info = "/bar";
243 let client_addr: SocketAddr = "127.0.0.1:8777".parse().unwrap();
244
245 let req_uri = format!(
246 "{}://{}{}/42{}?key1=value1&key2=value2",
247 scheme, host, component_path, path_info
248 );
249
250 let req = http::Request::builder()
251 .method("POST")
252 .uri(req_uri)
253 .body("")?;
254
255 let router = Router::build(
256 "/",
257 [(
258 &spin_http::routes::TriggerLookupKey::Component("DUMMY".into()),
259 &trigger_route.into(),
260 )],
261 None,
262 )?;
263 let route_match = router.route("/foo/42/bar")?;
264
265 let default_headers = compute_default_headers(req.uri(), host, &route_match, client_addr)?;
266
267 assert_eq!(
268 search(&FULL_URL, &default_headers).unwrap(),
269 "https://fermyon.dev/foo/42/bar?key1=value1&key2=value2".to_string()
270 );
271 assert_eq!(
272 search(&PATH_INFO, &default_headers).unwrap(),
273 "/bar".to_string()
274 );
275 assert_eq!(
276 search(&MATCHED_ROUTE, &default_headers).unwrap(),
277 "/foo/:userid/...".to_string()
278 );
279 assert_eq!(
280 search(&BASE_PATH, &default_headers).unwrap(),
281 "/".to_string()
282 );
283 assert_eq!(
284 search(&RAW_COMPONENT_ROUTE, &default_headers).unwrap(),
285 "/foo/:userid/...".to_string()
286 );
287 assert_eq!(
288 search(&COMPONENT_ROUTE, &default_headers).unwrap(),
289 "/foo/:userid".to_string()
290 );
291 assert_eq!(
292 search(&CLIENT_ADDR, &default_headers).unwrap(),
293 "127.0.0.1:8777".to_string()
294 );
295
296 assert_eq!(
297 search(
298 &["SPIN_PATH_MATCH_USERID", "X_PATH_MATCH_USERID"],
299 &default_headers
300 )
301 .unwrap(),
302 "42".to_string()
303 );
304
305 Ok(())
306 }
307
308 #[test]
309 fn forbidden_headers_are_removed() {
310 let mut req = Request::get("http://test.spin.internal")
311 .header("Host", "test.spin.internal")
312 .header("accept", "text/plain")
313 .body(Default::default())
314 .unwrap();
315
316 strip_forbidden_headers(&mut req);
317
318 assert_eq!(1, req.headers().len());
319 assert!(req.headers().get("Host").is_none());
320
321 let mut req = Request::get("http://test.spin.internal")
322 .header("Host", "test.spin.internal")
323 .header("accept", "text/plain")
324 .header("connection", "the knee bone and the leg bone")
325 .body(Default::default())
326 .unwrap();
327
328 strip_forbidden_headers(&mut req);
329
330 assert_eq!(1, req.headers().len());
331 assert!(req.headers().get("Host").is_none());
332 assert!(req.headers().get("Connection").is_none());
333
334 let mut req = Request::get("http://test.spin.internal")
335 .header("Host", "test.spin.internal:1234")
336 .header("accept", "text/plain")
337 .header("Connection", "the knee bone and the leg bone")
338 .body(Default::default())
339 .unwrap();
340
341 strip_forbidden_headers(&mut req);
342
343 assert_eq!(1, req.headers().len());
344 assert!(req.headers().get("Host").is_none());
345 assert!(req.headers().get("connection").is_none());
346 }
347
348 #[test]
349 fn non_forbidden_headers_are_not_removed() {
350 let mut req = Request::get("http://test.example.com")
351 .header("Host", "test.example.org")
352 .header("accept", "text/plain")
353 .body(Default::default())
354 .unwrap();
355
356 strip_forbidden_headers(&mut req);
357
358 assert_eq!(2, req.headers().len());
359 assert!(req.headers().get("Host").is_some());
360 }
361
362 fn search(
363 keys: &[&str; 2],
364 headers: &[([Cow<'static, str>; 2], Cow<'_, str>)],
365 ) -> Option<String> {
366 let mut res: Option<String> = None;
367 for (k, v) in headers {
368 if k[0] == keys[0] && k[1] == keys[1] {
369 res = Some(v.as_ref().to_owned());
370 }
371 }
372
373 res
374 }
375}