Skip to content

Commit cd64556

Browse files
authored
Merge pull request #2737 from lann/factors-update-outbound-net
factors: Update outbound networking
2 parents ca0ba2d + f09aaff commit cd64556

File tree

19 files changed

+346
-192
lines changed

19 files changed

+346
-192
lines changed

Cargo.lock

Lines changed: 132 additions & 56 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/factor-outbound-http/src/lib.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@ pub use wasmtime_wasi_http::{
2525
HttpResult,
2626
};
2727

28-
pub struct OutboundHttpFactor;
28+
#[derive(Default)]
29+
pub struct OutboundHttpFactor {
30+
_priv: (),
31+
}
32+
33+
impl OutboundHttpFactor {
34+
pub fn new() -> Self {
35+
Self::default()
36+
}
37+
}
2938

3039
impl Factor for OutboundHttpFactor {
3140
type RuntimeConfig = ();
@@ -60,6 +69,7 @@ impl Factor for OutboundHttpFactor {
6069
wasi_http_ctx: WasiHttpCtx::new(),
6170
allowed_hosts,
6271
component_tls_configs,
72+
self_request_origin: None,
6373
request_interceptor: None,
6474
})
6575
}
@@ -69,10 +79,19 @@ pub struct InstanceState {
6979
wasi_http_ctx: WasiHttpCtx,
7080
allowed_hosts: OutboundAllowedHosts,
7181
component_tls_configs: ComponentTlsConfigs,
82+
self_request_origin: Option<SelfRequestOrigin>,
7283
request_interceptor: Option<Box<dyn OutboundHttpInterceptor>>,
7384
}
7485

7586
impl InstanceState {
87+
/// Sets the [`SelfRequestOrigin`] for this instance.
88+
///
89+
/// This is used to handle outbound requests to relative URLs. If unset,
90+
/// those requests will fail.
91+
pub fn set_self_request_origin(&mut self, origin: SelfRequestOrigin) {
92+
self.self_request_origin = Some(origin);
93+
}
94+
7695
/// Sets a [`OutboundHttpInterceptor`] for this instance.
7796
///
7897
/// Returns an error if it has already been called for this instance.

crates/factor-outbound-http/src/spin.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use spin_factor_outbound_networking::OutboundUrl;
21
use spin_world::{
32
async_trait,
43
v1::http,
@@ -9,8 +8,7 @@ use spin_world::{
98
impl http::Host for crate::InstanceState {
109
async fn send_request(&mut self, req: Request) -> Result<Response, HttpError> {
1110
// FIXME(lann): This is all just a stub to test allowed_outbound_hosts
12-
let outbound_url = OutboundUrl::parse(&req.uri, "https").or(Err(HttpError::InvalidUrl))?;
13-
match self.allowed_hosts.allows(&outbound_url).await {
11+
match self.allowed_hosts.check_url(&req.uri, "https").await {
1412
Ok(true) => (),
1513
_ => {
1614
return Err(HttpError::DestinationNotAllowed);

crates/factor-outbound-http/src/wasi.rs

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use std::{error::Error, sync::Arc};
22

33
use anyhow::Context;
4-
use http::{header::HOST, uri::Authority, Request, Uri};
4+
use http::{header::HOST, Request};
55
use http_body_util::BodyExt;
66
use rustls::ClientConfig;
7-
use spin_factor_outbound_networking::{OutboundAllowedHosts, OutboundUrl};
7+
use spin_factor_outbound_networking::OutboundAllowedHosts;
88
use spin_factors::{wasmtime::component::ResourceTable, RuntimeFactorsInstanceState};
99
use tokio::{net::TcpStream, time::timeout};
10-
use tracing::Instrument;
10+
use tracing::{field::Empty, instrument, Instrument};
1111
use wasmtime_wasi_http::{
1212
bindings::http::types::ErrorCode,
1313
body::HyperOutgoingBody,
@@ -68,6 +68,19 @@ impl<'a> WasiHttpView for WasiHttpImplInner<'a> {
6868
self.table
6969
}
7070

71+
#[instrument(
72+
name = "spin_outbound_http.send_request",
73+
skip_all,
74+
fields(
75+
otel.kind = "client",
76+
url.full = %request.uri(),
77+
http.request.method = %request.method(),
78+
otel.name = %request.method(),
79+
http.response.status_code = Empty,
80+
server.address = Empty,
81+
server.port = Empty,
82+
),
83+
)]
7184
fn send_request(
7285
&mut self,
7386
mut request: Request<wasmtime_wasi_http::body::HyperOutgoingBody>,
@@ -93,6 +106,7 @@ impl<'a> WasiHttpView for WasiHttpImplInner<'a> {
93106
request,
94107
config,
95108
self.state.allowed_hosts.clone(),
109+
self.state.self_request_origin.clone(),
96110
tls_client_config,
97111
)
98112
.in_current_span(),
@@ -104,67 +118,52 @@ impl<'a> WasiHttpView for WasiHttpImplInner<'a> {
104118
async fn send_request_impl(
105119
mut request: Request<wasmtime_wasi_http::body::HyperOutgoingBody>,
106120
mut config: wasmtime_wasi_http::types::OutgoingRequestConfig,
107-
allowed_hosts: OutboundAllowedHosts,
121+
outbound_allowed_hosts: OutboundAllowedHosts,
122+
self_request_origin: Option<SelfRequestOrigin>,
108123
tls_client_config: Arc<ClientConfig>,
109124
) -> anyhow::Result<Result<IncomingResponse, ErrorCode>> {
110-
let allowed_hosts = allowed_hosts.resolve().await?;
111-
112-
let is_relative_url = request.uri().authority().is_none();
113-
if is_relative_url {
114-
if !allowed_hosts.allows_relative_url(&["http", "https"]) {
115-
return Ok(handle_not_allowed(request.uri(), true));
125+
if request.uri().authority().is_some() {
126+
// Absolute URI
127+
let is_allowed = outbound_allowed_hosts
128+
.check_url(&request.uri().to_string(), "https")
129+
.await
130+
.unwrap_or(false);
131+
if !is_allowed {
132+
return Ok(Err(ErrorCode::HttpRequestDenied));
133+
}
134+
} else {
135+
// Relative URI ("self" request)
136+
let is_allowed = outbound_allowed_hosts
137+
.check_relative_url(&["http", "https"])
138+
.await
139+
.unwrap_or(false);
140+
if !is_allowed {
141+
return Ok(Err(ErrorCode::HttpRequestDenied));
116142
}
117143

118-
let origin = request
119-
.extensions()
120-
.get::<SelfRequestOrigin>()
121-
.cloned()
122-
.context("cannot send relative outbound request; no 'origin' set by host")?;
144+
let Some(origin) = self_request_origin else {
145+
tracing::error!("Couldn't handle outbound HTTP request to relative URI; no origin set");
146+
return Ok(Err(ErrorCode::HttpRequestUriInvalid));
147+
};
123148

124149
config.use_tls = origin.use_tls();
125150

126151
request.headers_mut().insert(HOST, origin.host_header());
127152

128153
let path_and_query = request.uri().path_and_query().cloned();
129154
*request.uri_mut() = origin.into_uri(path_and_query);
130-
} else {
131-
let outbound_url = OutboundUrl::parse(request.uri().to_string(), "https")
132-
.map_err(|_| ErrorCode::HttpRequestUriInvalid)?;
133-
if !allowed_hosts.allows(&outbound_url) {
134-
return Ok(handle_not_allowed(request.uri(), false));
135-
}
136155
}
137156

138-
if let Some(authority) = request.uri().authority() {
139-
let current_span = tracing::Span::current();
140-
current_span.record("server.address", authority.host());
141-
if let Some(port) = authority.port() {
142-
current_span.record("server.port", port.as_u16());
143-
}
157+
let authority = request.uri().authority().context("authority not set")?;
158+
let current_span = tracing::Span::current();
159+
current_span.record("server.address", authority.host());
160+
if let Some(port) = authority.port() {
161+
current_span.record("server.port", port.as_u16());
144162
}
145163

146164
Ok(send_request_handler(request, config, tls_client_config).await)
147165
}
148166

149-
// TODO(factors): Move to some callback on spin-factor-outbound-networking (?)
150-
fn handle_not_allowed(uri: &Uri, is_relative: bool) -> Result<IncomingResponse, ErrorCode> {
151-
tracing::error!("Destination not allowed!: {uri}");
152-
let allowed_host_example = if is_relative {
153-
terminal::warn!("A component tried to make a HTTP request to the same component but it does not have permission.");
154-
"http://self".to_string()
155-
} else {
156-
let host = format!(
157-
"{scheme}://{authority}",
158-
scheme = uri.scheme_str().unwrap_or_default(),
159-
authority = uri.authority().map(Authority::as_str).unwrap_or_default()
160-
);
161-
terminal::warn!("A component tried to make a HTTP request to non-allowed host '{host}'.");
162-
host
163-
};
164-
eprintln!("To allow requests, add 'allowed_outbound_hosts = [\"{allowed_host_example}\"]' to the manifest component section.");
165-
Err(ErrorCode::HttpRequestDenied)
166-
}
167-
168167
/// This is a fork of wasmtime_wasi_http::default_send_request_handler function
169168
/// forked from bytecodealliance/wasmtime commit-sha 29a76b68200fcfa69c8fb18ce6c850754279a05b
170169
/// This fork provides the ability to configure client cert auth for mTLS

crates/factor-outbound-http/tests/factor_test.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,11 @@ async fn allowed_host_is_allowed() -> anyhow::Result<()> {
4040
#[tokio::test]
4141
async fn self_request_smoke_test() -> anyhow::Result<()> {
4242
let mut state = test_instance_state("http://self").await?;
43-
let mut wasi_http = OutboundHttpFactor::get_wasi_http_impl(&mut state).unwrap();
43+
let origin = SelfRequestOrigin::from_uri(&Uri::from_static("http://[100::1]"))?;
44+
state.http.set_self_request_origin(origin);
4445

45-
let mut req = Request::get("/self-request").body(Default::default())?;
46-
let origin = Uri::from_static("http://[100::1]");
47-
req.extensions_mut()
48-
.insert(SelfRequestOrigin::from_uri(&origin).unwrap());
46+
let mut wasi_http = OutboundHttpFactor::get_wasi_http_impl(&mut state).unwrap();
47+
let req = Request::get("/self-request").body(Default::default())?;
4948
let mut future_resp = wasi_http.send_request(req, test_request_config())?;
5049
future_resp.ready().await;
5150

@@ -77,8 +76,8 @@ async fn test_instance_state(
7776
) -> anyhow::Result<TestFactorsInstanceState> {
7877
let factors = TestFactors {
7978
variables: VariablesFactor::default(),
80-
networking: OutboundNetworkingFactor,
81-
http: OutboundHttpFactor,
79+
networking: OutboundNetworkingFactor::new(),
80+
http: OutboundHttpFactor::new(),
8281
};
8382
let env = TestEnvironment::new(factors).extend_manifest(toml! {
8483
[component.test-component]

crates/factor-outbound-mqtt/tests/factor_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ struct TestFactors {
4646
fn factors() -> TestFactors {
4747
TestFactors {
4848
variables: VariablesFactor::default(),
49-
networking: OutboundNetworkingFactor,
49+
networking: OutboundNetworkingFactor::new(),
5050
mqtt: OutboundMqttFactor::new(Arc::new(MockMqttClient {})),
5151
}
5252
}

crates/factor-outbound-mysql/tests/factor_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ struct TestFactors {
2020
fn factors() -> TestFactors {
2121
TestFactors {
2222
variables: VariablesFactor::default(),
23-
networking: OutboundNetworkingFactor,
23+
networking: OutboundNetworkingFactor::new(),
2424
mysql: OutboundMysqlFactor::<MockClient>::new(),
2525
}
2626
}

0 commit comments

Comments
 (0)