Skip to content

Commit ff2bfd4

Browse files
committed
allow outbound calls to components in same spin app
+ use special value `self` in allowed_http_hosts to allow components in the same spin app to make outbound http requests to each other + make requests with relative routes in app once self is set + update example for rust outbound http to demonstrate capability + adds e2e + resolves #957 + resolves #1533 Signed-off-by: Michelle Dhanani <michelle@fermyon.com>
1 parent 2617020 commit ff2bfd4

File tree

26 files changed

+338
-518
lines changed

26 files changed

+338
-518
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/outbound-http/src/allowed_http_hosts.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ impl AllowedHttpHosts {
2626
Self::AllowSpecific(hosts) => hosts.iter().any(|h| h.allow(url)),
2727
}
2828
}
29+
30+
pub fn includes(&self, host: AllowedHttpHost) -> bool {
31+
match self {
32+
Self::AllowAll => true,
33+
Self::AllowSpecific(hosts) => hosts.contains(&host),
34+
}
35+
}
2936
}
3037

3138
/// An HTTP host allow-list entry.
@@ -214,6 +221,14 @@ mod test {
214221
);
215222
}
216223

224+
#[test]
225+
fn test_allowed_hosts_accepts_self() {
226+
assert_eq!(
227+
AllowedHttpHost::host("self"),
228+
parse_allowed_http_host("self").unwrap()
229+
);
230+
}
231+
217232
#[test]
218233
fn test_allowed_hosts_accepts_localhost_addresses() {
219234
assert_eq!(
@@ -303,4 +318,13 @@ mod test {
303318
assert!(!allowed.allow(&Url::parse("http://example.com/").unwrap()));
304319
assert!(!allowed.allow(&Url::parse("http://google.com/").unwrap()));
305320
}
321+
322+
#[test]
323+
fn test_allowed_hosts_includes() {
324+
let allowed =
325+
parse_allowed_http_hosts(&to_vec_owned(&["self", "http://example.com:8383"])).unwrap();
326+
assert!(allowed.includes(AllowedHttpHost::host("self")));
327+
assert!(allowed.includes(AllowedHttpHost::host_and_port("example.com", 8383)));
328+
assert!(!allowed.includes(AllowedHttpHost::host("google.com")));
329+
}
306330
}

crates/outbound-http/src/lib.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use spin_world::{
1111
http_types::{Headers, HttpError, Method, Request, Response},
1212
};
1313

14-
use allowed_http_hosts::AllowedHttpHosts;
14+
use allowed_http_hosts::{AllowedHttpHost, AllowedHttpHosts};
1515
pub use host_component::OutboundHttpComponent;
1616

1717
pub const ALLOWED_HTTP_HOSTS_KEY: MetadataKey<Vec<String>> = MetadataKey::new("allowed_http_hosts");
@@ -21,15 +21,27 @@ pub const ALLOWED_HTTP_HOSTS_KEY: MetadataKey<Vec<String>> = MetadataKey::new("a
2121
pub struct OutboundHttp {
2222
/// List of hosts guest modules are allowed to make requests to.
2323
pub allowed_hosts: AllowedHttpHosts,
24+
/// During an incoming HTTP request, origin is set to the host of that incoming HTTP request.
25+
/// This is used to direct outbound requests to the same host when allowed.
26+
pub origin: String,
2427
client: Option<Client>,
2528
}
2629

2730
impl OutboundHttp {
2831
/// Check if guest module is allowed to send request to URL, based on the list of
29-
/// allowed hosts defined by the runtime. If the list of allowed hosts contains
32+
/// allowed hosts defined by the runtime. If the url passed in is a relative path,
33+
/// only allow if allowed_hosts contains `self`. If the list of allowed hosts contains
3034
/// `insecure:allow-all`, then all hosts are allowed.
3135
/// If `None` is passed, the guest module is not allowed to send the request.
32-
fn is_allowed(&self, url: &str) -> Result<bool, HttpError> {
36+
fn is_allowed(&mut self, url: &str) -> Result<bool, HttpError> {
37+
if url.starts_with('/') {
38+
if self.allowed_hosts.includes(AllowedHttpHost::host("self")) {
39+
return Ok(true);
40+
} else {
41+
return Ok(false);
42+
}
43+
}
44+
3345
let url = Url::parse(url).map_err(|_| HttpError::InvalidUrl)?;
3446
Ok(self.allowed_hosts.allow(&url))
3547
}
@@ -49,7 +61,14 @@ impl outbound_http::Host for OutboundHttp {
4961
}
5062

5163
let method = method_from(req.method);
52-
let url = Url::parse(&req.uri).map_err(|_| HttpError::InvalidUrl)?;
64+
65+
let req_url: Url = if req.uri.starts_with('/') {
66+
Url::parse(&format!("{}{}", self.origin, req.uri))
67+
.map_err(|_| HttpError::InvalidUrl)?
68+
} else {
69+
Url::parse(&req.uri).map_err(|_| HttpError::InvalidUrl)?
70+
};
71+
5372
let headers = request_headers(req.headers).map_err(|_| HttpError::RuntimeError)?;
5473
let body = req.body.unwrap_or_default().to_vec();
5574

@@ -62,7 +81,7 @@ impl outbound_http::Host for OutboundHttp {
6281
let client = self.client.get_or_insert_with(Default::default);
6382

6483
let resp = client
65-
.request(method, url)
84+
.request(method, req_url)
6685
.headers(headers)
6786
.body(body)
6887
.send()

crates/trigger-http/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ futures-util = "0.3.8"
1616
http = "0.2"
1717
hyper = { version = "0.14", features = ["full"] }
1818
indexmap = "1"
19+
outbound-http = { path = "../outbound-http" }
1920
percent-encoding = "2"
2021
rustls-pemfile = "0.3.0"
2122
serde = { version = "1.0", features = ["derive"] }

crates/trigger-http/src/spin.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,27 @@ impl HttpExecutor for SpinHttpExecutor {
2727
component_id
2828
);
2929

30-
let (instance, store) = engine.prepare_instance(component_id).await?;
30+
let (instance, mut store) = engine.prepare_instance(component_id).await?;
3131
let EitherInstance::Component(instance) = instance else {
3232
unreachable!()
3333
};
3434

35+
if let Some(authority) = req.uri().authority() {
36+
if let Some(scheme) = req.uri().scheme_str() {
37+
if let Some(outbound_http_handle) = engine
38+
.engine
39+
.find_host_component_handle::<std::sync::Arc<outbound_http::OutboundHttpComponent>>(
40+
)
41+
{
42+
let outbound_http_data = store
43+
.host_components_data()
44+
.get_or_insert(outbound_http_handle);
45+
46+
outbound_http_data.origin = format!("{}://{}", scheme, authority);
47+
}
48+
}
49+
}
50+
3551
let resp = Self::execute_impl(store, instance, base, raw_route, req, client_addr)
3652
.await
3753
.map_err(contextualise_err)?;

0 commit comments

Comments
 (0)