Skip to content

Commit 7d28445

Browse files
authored
Merge pull request #3269 from lann/override-connect-host
factor-outbound-http: Add InterceptRequest::override_connect_host
2 parents e8212c2 + ad0a476 commit 7d28445

File tree

3 files changed

+64
-7
lines changed

3 files changed

+64
-7
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub enum InterceptOutcome {
3939
pub struct InterceptRequest {
4040
inner: Request<()>,
4141
body: InterceptBody,
42+
pub(crate) override_connect_host: Option<String>,
4243
}
4344

4445
enum InterceptBody {
@@ -47,6 +48,18 @@ enum InterceptBody {
4748
}
4849

4950
impl InterceptRequest {
51+
/// Overrides the host that will be connected to for this outbound request.
52+
///
53+
/// This override does not have any effect on TLS server name checking or
54+
/// HTTP authority / host headers.
55+
///
56+
/// This host will not be checked against `allowed_outbound_hosts`; if that
57+
/// check should occur it must be performed by the interceptor. The resolved
58+
/// IP addresses from this host will be checked against blocked IP networks.
59+
pub fn override_connect_host(&mut self, host: impl Into<String>) {
60+
self.override_connect_host = Some(host.into())
61+
}
62+
5063
pub fn into_hyper_request(self) -> Request<HyperBody> {
5164
let (parts, ()) = self.inner.into_parts();
5265
Request::from_parts(parts, self.body.into())
@@ -81,6 +94,7 @@ impl From<Request<HyperBody>> for InterceptRequest {
8194
Self {
8295
inner: Request::from_parts(parts, ()),
8396
body: InterceptBody::Hyper(body),
97+
override_connect_host: None,
8498
}
8599
}
86100
}
@@ -91,6 +105,7 @@ impl From<Request<Vec<u8>>> for InterceptRequest {
91105
Self {
92106
inner: Request::from_parts(parts, ()),
93107
body: InterceptBody::Vec(body),
108+
override_connect_host: None,
94109
}
95110
}
96111
}

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,12 @@ impl RequestSender {
166166
spin_telemetry::inject_trace_context(&mut request);
167167

168168
// Run any configured request interceptor
169+
let mut override_connect_host = None;
169170
if let Some(interceptor) = &self.request_interceptor {
170171
let intercept_request = std::mem::take(&mut request).into();
171172
match interceptor.intercept(intercept_request).await? {
172-
InterceptOutcome::Continue(req) => {
173+
InterceptOutcome::Continue(mut req) => {
174+
override_connect_host = req.override_connect_host.take();
173175
request = req.into_hyper_request();
174176
}
175177
InterceptOutcome::Complete(resp) => {
@@ -186,13 +188,16 @@ impl RequestSender {
186188
// Backfill span fields after potentially updating the URL in the interceptor
187189
if let Some(authority) = request.uri().authority() {
188190
let span = tracing::Span::current();
189-
span.record("server.address", authority.host());
191+
let host = override_connect_host.as_deref().unwrap_or(authority.host());
192+
span.record("server.address", host);
190193
if let Some(port) = authority.port() {
191194
span.record("server.port", port.as_u16());
192195
}
193196
}
194197

195-
Ok(self.send_request(request, config).await?)
198+
Ok(self
199+
.send_request(request, config, override_connect_host)
200+
.await?)
196201
}
197202

198203
async fn prepare_request(
@@ -270,6 +275,7 @@ impl RequestSender {
270275
self,
271276
request: OutgoingRequest,
272277
config: OutgoingRequestConfig,
278+
override_connect_host: Option<String>,
273279
) -> Result<IncomingResponse, ErrorCode> {
274280
let OutgoingRequestConfig {
275281
use_tls,
@@ -290,6 +296,7 @@ impl RequestSender {
290296
blocked_networks: self.blocked_networks,
291297
connect_timeout,
292298
tls_client_config,
299+
override_connect_host,
293300
},
294301
async move {
295302
if use_tls {
@@ -369,11 +376,16 @@ struct ConnectOptions {
369376
blocked_networks: BlockedNetworks,
370377
connect_timeout: Duration,
371378
tls_client_config: Option<TlsClientConfig>,
379+
override_connect_host: Option<String>,
372380
}
373381

374382
impl ConnectOptions {
375383
async fn connect_tcp(&self, uri: &Uri, default_port: u16) -> Result<TcpStream, ErrorCode> {
376-
let host = uri.host().ok_or(ErrorCode::HttpRequestUriInvalid)?;
384+
let host = self
385+
.override_connect_host
386+
.as_deref()
387+
.or(uri.host())
388+
.ok_or(ErrorCode::HttpRequestUriInvalid)?;
377389
let host_and_port = (host, uri.port_u16().unwrap_or(default_port));
378390

379391
let mut socket_addrs = tokio::net::lookup_host(host_and_port)

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

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ use anyhow::bail;
44
use http::{Request, Uri};
55
use spin_common::{assert_matches, assert_not_matches};
66
use spin_factor_outbound_http::{
7+
intercept::{InterceptOutcome, InterceptRequest, OutboundHttpInterceptor},
78
ErrorCode, HostFutureIncomingResponse, OutboundHttpFactor, SelfRequestOrigin,
89
};
910
use spin_factor_outbound_networking::OutboundNetworkingFactor;
1011
use spin_factor_variables::VariablesFactor;
1112
use spin_factors::{anyhow, RuntimeFactors};
1213
use spin_factors_test::{toml, TestEnvironment};
14+
use spin_world::async_trait;
1315
use wasmtime_wasi::p2::Pollable;
1416
use wasmtime_wasi_http::{types::OutgoingRequestConfig, WasiHttpView};
1517

@@ -98,6 +100,34 @@ async fn disallowed_private_ips_fails() -> anyhow::Result<()> {
98100
Ok(())
99101
}
100102

103+
#[tokio::test]
104+
async fn override_connect_host_disallowed_private_ip_fails() -> anyhow::Result<()> {
105+
let mut state = test_instance_state("http://*", false).await?;
106+
state.http.set_request_interceptor({
107+
struct Interceptor;
108+
#[async_trait]
109+
impl OutboundHttpInterceptor for Interceptor {
110+
async fn intercept(
111+
&self,
112+
mut request: InterceptRequest,
113+
) -> wasmtime_wasi_http::HttpResult<InterceptOutcome> {
114+
request.override_connect_host("localhost");
115+
Ok(InterceptOutcome::Continue(request))
116+
}
117+
}
118+
Interceptor
119+
})?;
120+
let mut wasi_http = OutboundHttpFactor::get_wasi_http_impl(&mut state).unwrap();
121+
let req = Request::get("http://1.1.1.1").body(Default::default())?;
122+
let mut future_resp = wasi_http.send_request(req, test_request_config())?;
123+
future_resp.ready().await;
124+
assert_matches!(
125+
future_resp.unwrap_ready().unwrap(),
126+
Err(ErrorCode::DestinationIpProhibited),
127+
);
128+
Ok(())
129+
}
130+
101131
async fn test_instance_state(
102132
allowed_outbound_hosts: &str,
103133
allow_private_ips: bool,
@@ -128,9 +158,9 @@ async fn test_instance_state(
128158
fn test_request_config() -> OutgoingRequestConfig {
129159
OutgoingRequestConfig {
130160
use_tls: false,
131-
connect_timeout: Duration::from_millis(1),
132-
first_byte_timeout: Duration::from_millis(1),
133-
between_bytes_timeout: Duration::from_millis(1),
161+
connect_timeout: Duration::from_millis(10),
162+
first_byte_timeout: Duration::from_millis(10),
163+
between_bytes_timeout: Duration::from_millis(10),
134164
}
135165
}
136166

0 commit comments

Comments
 (0)