Skip to content

Commit 0ae6ec4

Browse files
authored
Merge pull request #1913 from itowlson/dep-breaking-loader
Break `loader` dependency on wasmtime
2 parents 9fb777b + a0beaf1 commit 0ae6ec4

File tree

7 files changed

+190
-181
lines changed

7 files changed

+190
-181
lines changed

Cargo.lock

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

crates/loader/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ glob = "0.3.0"
1515
itertools = "0.10.3"
1616
lazy_static = "1.4.0"
1717
mime_guess = { version = "2.0" }
18-
outbound-http = { path = "../outbound-http" }
18+
outbound-http = { path = "../outbound-http", default-features = false }
1919
path-absolutize = "3.0.11"
2020
regex = "1.5.4"
2121
reqwest = "0.11.9"
@@ -26,7 +26,6 @@ sha2 = "0.10.8"
2626
shellexpand = "3.1"
2727
spin-locked-app = { path = "../locked-app" }
2828
spin-common = { path = "../common" }
29-
spin-config = { path = "../config" }
3029
spin-manifest = { path = "../manifest" }
3130
tempfile = "3.8.0"
3231
terminal = { path = "../terminal" }

crates/outbound-http/Cargo.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ doctest = false
1111
anyhow = "1.0"
1212
http = "0.2"
1313
reqwest = { version = "0.11", features = ["gzip"] }
14-
spin-app = { path = "../app" }
15-
spin-core = { path = "../core" }
16-
spin-world = { path = "../world" }
14+
spin-app = { path = "../app", optional = true }
15+
spin-core = { path = "../core", optional = true }
16+
spin-locked-app = { path = "../locked-app" }
17+
spin-world = { path = "../world", optional = true }
1718
terminal = { path = "../terminal" }
1819
tracing = { workspace = true }
1920
url = "2.2.1"
21+
22+
[features]
23+
default = ["runtime"]
24+
runtime = ["dep:spin-app", "dep:spin-core", "dep:spin-world"]

crates/outbound-http/src/host_component.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use spin_app::DynamicHostComponent;
44
use spin_core::{Data, HostComponent, Linker};
55
use spin_world::v1::http;
66

7-
use crate::{allowed_http_hosts::parse_allowed_http_hosts, OutboundHttp};
7+
use crate::{allowed_http_hosts::parse_allowed_http_hosts, host_impl::OutboundHttp};
88

99
pub struct OutboundHttpComponent;
1010

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use anyhow::Result;
2+
use http::HeaderMap;
3+
use reqwest::{Client, Url};
4+
use spin_core::async_trait;
5+
use spin_world::v1::{
6+
http as outbound_http,
7+
http_types::{Headers, HttpError, Method, Request, Response},
8+
};
9+
10+
use crate::allowed_http_hosts::AllowedHttpHosts;
11+
12+
/// A very simple implementation for outbound HTTP requests.
13+
#[derive(Default, Clone)]
14+
pub struct OutboundHttp {
15+
/// List of hosts guest modules are allowed to make requests to.
16+
pub allowed_hosts: AllowedHttpHosts,
17+
/// During an incoming HTTP request, origin is set to the host of that incoming HTTP request.
18+
/// This is used to direct outbound requests to the same host when allowed.
19+
pub origin: String,
20+
client: Option<Client>,
21+
}
22+
23+
impl OutboundHttp {
24+
/// Check if guest module is allowed to send request to URL, based on the list of
25+
/// allowed hosts defined by the runtime. If the url passed in is a relative path,
26+
/// only allow if allowed_hosts contains `self`. If the list of allowed hosts contains
27+
/// `insecure:allow-all`, then all hosts are allowed.
28+
/// If `None` is passed, the guest module is not allowed to send the request.
29+
fn is_allowed(&mut self, url: &str) -> Result<bool, HttpError> {
30+
if url.starts_with('/') {
31+
return Ok(self.allowed_hosts.allow_relative_url());
32+
}
33+
34+
let url = Url::parse(url).map_err(|_| HttpError::InvalidUrl)?;
35+
Ok(self.allowed_hosts.allow(&url))
36+
}
37+
}
38+
39+
#[async_trait]
40+
impl outbound_http::Host for OutboundHttp {
41+
async fn send_request(&mut self, req: Request) -> Result<Result<Response, HttpError>> {
42+
Ok(async {
43+
tracing::log::trace!("Attempting to send outbound HTTP request to {}", req.uri);
44+
if !self
45+
.is_allowed(&req.uri)
46+
.map_err(|_| HttpError::RuntimeError)?
47+
{
48+
tracing::log::info!("Destination not allowed: {}", req.uri);
49+
if let Some(host) = host(&req.uri) {
50+
terminal::warn!("A component tried to make a HTTP request to non-allowed host '{host}'.");
51+
eprintln!("To allow requests, add 'allowed_http_hosts = [\"{host}\"]' to the manifest component section.");
52+
}
53+
return Err(HttpError::DestinationNotAllowed);
54+
}
55+
56+
let method = method_from(req.method);
57+
58+
let abs_url = if req.uri.starts_with('/') {
59+
format!("{}{}", self.origin, req.uri)
60+
} else {
61+
req.uri.clone()
62+
};
63+
64+
let req_url = Url::parse(&abs_url).map_err(|_| HttpError::InvalidUrl)?;
65+
66+
let headers = request_headers(req.headers).map_err(|_| HttpError::RuntimeError)?;
67+
let body = req.body.unwrap_or_default().to_vec();
68+
69+
if !req.params.is_empty() {
70+
tracing::log::warn!("HTTP params field is deprecated");
71+
}
72+
73+
// Allow reuse of Client's internal connection pool for multiple requests
74+
// in a single component execution
75+
let client = self.client.get_or_insert_with(Default::default);
76+
77+
let resp = client
78+
.request(method, req_url)
79+
.headers(headers)
80+
.body(body)
81+
.send()
82+
.await
83+
.map_err(log_reqwest_error)?;
84+
tracing::log::trace!("Returning response from outbound request to {}", req.uri);
85+
response_from_reqwest(resp).await
86+
}
87+
.await)
88+
}
89+
}
90+
91+
fn log_reqwest_error(err: reqwest::Error) -> HttpError {
92+
let error_desc = if err.is_timeout() {
93+
"timeout error"
94+
} else if err.is_connect() {
95+
"connection error"
96+
} else if err.is_body() || err.is_decode() {
97+
"message body error"
98+
} else if err.is_request() {
99+
"request error"
100+
} else {
101+
"error"
102+
};
103+
tracing::warn!(
104+
"Outbound HTTP {}: URL {}, error detail {:?}",
105+
error_desc,
106+
err.url()
107+
.map(|u| u.to_string())
108+
.unwrap_or_else(|| "<unknown>".to_owned()),
109+
err
110+
);
111+
HttpError::RuntimeError
112+
}
113+
114+
fn method_from(m: Method) -> http::Method {
115+
match m {
116+
Method::Get => http::Method::GET,
117+
Method::Post => http::Method::POST,
118+
Method::Put => http::Method::PUT,
119+
Method::Delete => http::Method::DELETE,
120+
Method::Patch => http::Method::PATCH,
121+
Method::Head => http::Method::HEAD,
122+
Method::Options => http::Method::OPTIONS,
123+
}
124+
}
125+
126+
async fn response_from_reqwest(res: reqwest::Response) -> Result<Response, HttpError> {
127+
let status = res.status().as_u16();
128+
let headers = response_headers(res.headers()).map_err(|_| HttpError::RuntimeError)?;
129+
130+
let body = Some(
131+
res.bytes()
132+
.await
133+
.map_err(|_| HttpError::RuntimeError)?
134+
.to_vec(),
135+
);
136+
137+
Ok(Response {
138+
status,
139+
headers,
140+
body,
141+
})
142+
}
143+
144+
fn request_headers(h: Headers) -> anyhow::Result<HeaderMap> {
145+
let mut res = HeaderMap::new();
146+
for (k, v) in h {
147+
res.insert(
148+
http::header::HeaderName::try_from(k)?,
149+
http::header::HeaderValue::try_from(v)?,
150+
);
151+
}
152+
Ok(res)
153+
}
154+
155+
fn response_headers(h: &HeaderMap) -> anyhow::Result<Option<Vec<(String, String)>>> {
156+
let mut res: Vec<(String, String)> = vec![];
157+
158+
for (k, v) in h {
159+
res.push((
160+
k.to_string(),
161+
std::str::from_utf8(v.as_bytes())?.to_string(),
162+
));
163+
}
164+
165+
Ok(Some(res))
166+
}
167+
168+
fn host(url: &str) -> Option<String> {
169+
url::Url::parse(url)
170+
.ok()
171+
.and_then(|u| u.host().map(|h| h.to_string()))
172+
}

0 commit comments

Comments
 (0)